From 6046dfb32eff0723e593dd6ae28a6601da98acb1 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:28:34 -0700 Subject: [PATCH 01/69] re-format: src/navigation/restaurant --- src/navigation/restaurant/Dashboard.js | 190 ++++++++++-------- src/navigation/restaurant/Date.js | 31 +-- src/navigation/restaurant/List.js | 73 +++---- src/navigation/restaurant/LoopeatFormats.js | 119 ++++++----- src/navigation/restaurant/Menus.js | 120 ++++++----- src/navigation/restaurant/OpeningHours.js | 137 +++++++------ src/navigation/restaurant/Order.js | 128 +++++++----- src/navigation/restaurant/OrderCancel.js | 104 ++++++---- src/navigation/restaurant/OrderDelay.js | 57 +++--- src/navigation/restaurant/OrderRefuse.js | 88 ++++---- src/navigation/restaurant/Printer.js | 158 +++++++++------ src/navigation/restaurant/ProductOptions.js | 76 +++---- src/navigation/restaurant/Products.js | 93 +++++---- src/navigation/restaurant/Settings.js | 105 +++++----- .../restaurant/SettingsNavigator.js | 31 ++- .../restaurant/components/BigButton.js | 48 ++--- .../restaurant/components/DatePickerHeader.js | 50 +++-- .../restaurant/components/HeaderRight.js | 66 +++--- .../components/OrderAcceptedFooter.js | 61 +++--- .../restaurant/components/OrderButtons.js | 67 +++--- .../restaurant/components/OrderHeading.js | 109 ++++++---- .../restaurant/components/OrderList.js | 92 +++++---- .../components/SwipeToAcceptOrRefuse.js | 73 ++++--- .../components/WebSocketIndicator.js | 26 ++- 24 files changed, 1192 insertions(+), 910 deletions(-) diff --git a/src/navigation/restaurant/Dashboard.js b/src/navigation/restaurant/Dashboard.js index 6d68a914c..439e24f8f 100644 --- a/src/navigation/restaurant/Dashboard.js +++ b/src/navigation/restaurant/Dashboard.js @@ -1,41 +1,49 @@ import React, { Component } from 'react'; import { Alert, InteractionManager, NativeModules, View } from 'react-native'; import { Center, VStack } from 'native-base'; -import { connect } from 'react-redux' -import { withTranslation } from 'react-i18next' -import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake' -import moment from 'moment' +import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; +import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake'; +import moment from 'moment'; -import DangerAlert from '../../components/DangerAlert' -import Offline from '../../components/Offline' +import DangerAlert from '../../components/DangerAlert'; +import Offline from '../../components/Offline'; -import OrderList from './components/OrderList' -import DatePickerHeader from './components/DatePickerHeader' -import WebSocketIndicator from './components/WebSocketIndicator' +import OrderList from './components/OrderList'; +import DatePickerHeader from './components/DatePickerHeader'; +import WebSocketIndicator from './components/WebSocketIndicator'; import { - changeDate, changeStatus, deleteOpeningHoursSpecification, loadOrderAndNavigate, loadOrders } from '../../redux/Restaurant/actions' -import { connect as connectCentrifugo } from '../../redux/middlewares/CentrifugoMiddleware/actions' + changeDate, + changeStatus, + deleteOpeningHoursSpecification, + loadOrderAndNavigate, + loadOrders, +} from '../../redux/Restaurant/actions'; +import { connect as connectCentrifugo } from '../../redux/middlewares/CentrifugoMiddleware/actions'; import { selectAcceptedOrders, selectCancelledOrders, selectFulfilledOrders, selectNewOrders, selectPickedOrders, - selectSpecialOpeningHoursSpecificationForToday } from '../../redux/Restaurant/selectors' -import { selectIsCentrifugoConnected, selectIsLoading } from '../../redux/App/selectors' -import PushNotification from '../../notifications' + selectSpecialOpeningHoursSpecificationForToday, +} from '../../redux/Restaurant/selectors'; +import { + selectIsCentrifugoConnected, + selectIsLoading, +} from '../../redux/App/selectors'; +import PushNotification from '../../notifications'; -const RNSound = NativeModules.RNSound +const RNSound = NativeModules.RNSound; class DashboardPage extends Component { - constructor(props) { - super(props) + super(props); this.state = { wasAlertShown: false, - } + }; } _checkSystemVolume() { @@ -48,8 +56,7 @@ class DashboardPage extends Component { { text: this.props.t('RESTAURANT_SOUND_ALERT_CONFIRM'), onPress: () => { - - this.setState({ wasAlertShown: true }) + this.setState({ wasAlertShown: true }); // If would be cool to open the device settings directly, // but it is not (yet) possible to sent an Intent with extra flags @@ -65,18 +72,17 @@ class DashboardPage extends Component { style: 'cancel', onPress: () => this.setState({ wasAlertShown: true }), }, - ] - ) + ], + ); } - }) + }); } componentDidMount() { - - activateKeepAwakeAsync() + activateKeepAwakeAsync(); if (!this.props.isCentrifugoConnected) { - this.props.connectCent() + this.props.connectCent(); } InteractionManager.runAfterInteractions(() => { @@ -100,105 +106,116 @@ class DashboardPage extends Component { // the home screen will be this one (for restaurants). // // @see https://rnfirebase.io/messaging/notifications#handling-interaction - PushNotification.getInitialNotification() - .then(remoteMessage => { - if (remoteMessage) { - const { event } = remoteMessage.data - if (event && event.name === 'order:created') { - this.props.loadOrderAndNavigate(event.data.order) - } + PushNotification.getInitialNotification().then(remoteMessage => { + if (remoteMessage) { + const { event } = remoteMessage.data; + if (event && event.name === 'order:created') { + this.props.loadOrderAndNavigate(event.data.order); } - }) - } - ) + } + }); + }, + ); } // setTimeout(() => this._checkSystemVolume(), 1500) - }) + }); } componentWillUnmount() { - deactivateKeepAwake() + deactivateKeepAwake(); } componentDidUpdate(prevProps) { + const hasRestaurantChanged = + this.props.restaurant !== prevProps.restaurant && + this.props.restaurant['@id'] !== prevProps.restaurant['@id']; - const hasRestaurantChanged = this.props.restaurant !== prevProps.restaurant - && this.props.restaurant['@id'] !== prevProps.restaurant['@id'] - - const hasChanged = this.props.date !== prevProps.date || hasRestaurantChanged + const hasChanged = + this.props.date !== prevProps.date || hasRestaurantChanged; if (hasChanged) { this.props.loadOrders( this.props.restaurant, - this.props.date.format('YYYY-MM-DD') - ) + this.props.date.format('YYYY-MM-DD'), + ); } // This is needed to display the title // WARNING Make sure to call navigation.setParams() only when needed to avoid infinite loop - const navRestaurant = this.props.route.params?.restaurant + const navRestaurant = this.props.route.params?.restaurant; if (!navRestaurant || navRestaurant !== this.props.restaurant) { - this.props.navigation.setParams({ restaurant: this.props.restaurant }) + this.props.navigation.setParams({ restaurant: this.props.restaurant }); } // Make sure to show Alert once loading has finished, // or it will be closed on iOS // https://github.com/facebook/react-native/issues/10471 - if (!this.state.wasAlertShown && !this.props.isLoading && prevProps.isLoading) { - this._checkSystemVolume() + if ( + !this.state.wasAlertShown && + !this.props.isLoading && + prevProps.isLoading + ) { + this._checkSystemVolume(); } } renderDashboard() { - - const { navigate } = this.props.navigation - const { date, restaurant, specialOpeningHoursSpecification } = this.props + const { navigate } = this.props.navigation; + const { date, restaurant, specialOpeningHoursSpecification } = this.props; return ( - - { restaurant.state === 'rush' && ( + + {restaurant.state === 'rush' && ( this.props.changeStatus(this.props.restaurant, 'normal') } /> + text={this.props.t('RESTAURANT_ALERT_RUSH_MODE_ON')} + onClose={() => + this.props.changeStatus(this.props.restaurant, 'normal') + } + /> )} - { specialOpeningHoursSpecification && ( + {specialOpeningHoursSpecification && ( this.props.deleteOpeningHoursSpecification(specialOpeningHoursSpecification) } /> + text={this.props.t('RESTAURANT_ALERT_CLOSED')} + onClose={() => + this.props.deleteOpeningHoursSpecification( + specialOpeningHoursSpecification, + ) + } + /> )} - - navigate('RestaurantDate') } - onTodayClick={ () => this.props.changeDate(moment()) } /> - navigate('RestaurantOrder', { order }) } /> + + navigate('RestaurantDate')} + onTodayClick={() => this.props.changeDate(moment())} + /> + navigate('RestaurantOrder', { order })} + /> - ) + ); } render() { - if (this.props.isInternetReachable) { - return this.renderDashboard() + return this.renderDashboard(); } return (
- ) + ); } } function mapStateToProps(state) { - return { httpClient: state.app.httpClient, baseURL: state.app.baseURL, @@ -210,22 +227,29 @@ function mapStateToProps(state) { fulfilledOrders: selectFulfilledOrders(state), date: state.restaurant.date, restaurant: state.restaurant.restaurant, - specialOpeningHoursSpecification: selectSpecialOpeningHoursSpecificationForToday(state), + specialOpeningHoursSpecification: + selectSpecialOpeningHoursSpecificationForToday(state), isInternetReachable: state.app.isInternetReachable, isLoading: selectIsLoading(state), isCentrifugoConnected: selectIsCentrifugoConnected(state), - } + }; } function mapDispatchToProps(dispatch) { return { - loadOrders: (restaurant, date, cb) => dispatch(loadOrders(restaurant, date, cb)), + loadOrders: (restaurant, date, cb) => + dispatch(loadOrders(restaurant, date, cb)), loadOrderAndNavigate: order => dispatch(loadOrderAndNavigate(order)), changeDate: date => dispatch(changeDate(date)), - changeStatus: (restaurant, state) => dispatch(changeStatus(restaurant, state)), - deleteOpeningHoursSpecification: openingHoursSpecification => dispatch(deleteOpeningHoursSpecification(openingHoursSpecification)), + changeStatus: (restaurant, state) => + dispatch(changeStatus(restaurant, state)), + deleteOpeningHoursSpecification: openingHoursSpecification => + dispatch(deleteOpeningHoursSpecification(openingHoursSpecification)), connectCent: () => dispatch(connectCentrifugo()), - } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(DashboardPage)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(DashboardPage)); diff --git a/src/navigation/restaurant/Date.js b/src/navigation/restaurant/Date.js index 48e97c617..b3260b6f0 100644 --- a/src/navigation/restaurant/Date.js +++ b/src/navigation/restaurant/Date.js @@ -1,42 +1,45 @@ -import React, { Component } from 'react' +import React, { Component } from 'react'; -import { withTranslation } from 'react-i18next' +import { withTranslation } from 'react-i18next'; -import { connect } from 'react-redux' -import { changeDate } from '../../redux/Restaurant/actions' -import { Calendar } from '../../components/Calendar' +import { connect } from 'react-redux'; +import { changeDate } from '../../redux/Restaurant/actions'; +import { Calendar } from '../../components/Calendar'; class DateScreen extends Component { - onDateChange(date) { - this.props.changeDate(date) - this.props.navigation.goBack() + this.props.changeDate(date); + this.props.navigation.goBack(); } render() { - return ( {this.onDateChange(momentDate)}} + onDateSelect={momentDate => { + this.onDateChange(momentDate); + }} /> - ) + ); } } function mapStateToProps(state) { return { date: state.restaurant.date, - } + }; } function mapDispatchToProps(dispatch) { return { changeDate: date => dispatch(changeDate(date)), - } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(DateScreen)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(DateScreen)); diff --git a/src/navigation/restaurant/List.js b/src/navigation/restaurant/List.js index 247231d6c..743c0a211 100644 --- a/src/navigation/restaurant/List.js +++ b/src/navigation/restaurant/List.js @@ -1,59 +1,57 @@ -import React, { Component } from 'react' -import { FlatList, StyleSheet, TouchableOpacity } from 'react-native' -import { - Box, HStack, Icon, Text, -} from 'native-base' -import { connect } from 'react-redux' -import { withTranslation } from 'react-i18next' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import React, { Component } from 'react'; +import { FlatList, StyleSheet, TouchableOpacity } from 'react-native'; +import { Box, HStack, Icon, Text } from 'native-base'; +import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import { changeRestaurant } from '../../redux/Restaurant/actions' -import ItemSeparator from '../../components/ItemSeparator' +import { changeRestaurant } from '../../redux/Restaurant/actions'; +import ItemSeparator from '../../components/ItemSeparator'; class ListScreen extends Component { - constructor(props) { - super(props) - this.state = { - } + super(props); + this.state = {}; } _onRestaurantClick(restaurant) { - this.props.changeRestaurant(restaurant) - this.props.navigation.navigate('RestaurantHome') + this.props.changeRestaurant(restaurant); + this.props.navigation.navigate('RestaurantHome'); } renderRestaurants() { - - const { restaurants, currentRestaurant } = this.props + const { restaurants, currentRestaurant } = this.props; return ( `${index}` } - ItemSeparatorComponent={ ItemSeparator } - data={ restaurants } - renderItem={ ({ item }) => ( - this._onRestaurantClick(item) }> + keyExtractor={(item, index) => `${index}`} + ItemSeparatorComponent={ItemSeparator} + data={restaurants} + renderItem={({ item }) => ( + this._onRestaurantClick(item)}> - { item.name } - { (item['@id'] === currentRestaurant['@id']) && } + {item.name} + {item['@id'] === currentRestaurant['@id'] && ( + + )} - )} /> - ) + )} + /> + ); } render() { return ( - - - { this.props.i18n.t('RESTAURANT_LIST_CLICK_BELOW') } + + + {this.props.i18n.t('RESTAURANT_LIST_CLICK_BELOW')} - { this.renderRestaurants() } + {this.renderRestaurants()} - ) + ); } } @@ -71,19 +69,22 @@ const styles = StyleSheet.create({ color: '#999', textAlign: 'center', }, -}) +}); function mapStateToProps(state) { return { restaurants: state.restaurant.myRestaurants, currentRestaurant: state.restaurant.restaurant, - } + }; } function mapDispatchToProps(dispatch) { return { changeRestaurant: restaurant => dispatch(changeRestaurant(restaurant)), - } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ListScreen)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(ListScreen)); diff --git a/src/navigation/restaurant/LoopeatFormats.js b/src/navigation/restaurant/LoopeatFormats.js index 7593b943a..444383877 100644 --- a/src/navigation/restaurant/LoopeatFormats.js +++ b/src/navigation/restaurant/LoopeatFormats.js @@ -1,92 +1,113 @@ -import React, { useEffect, useCallback } from 'react' -import { FlatList } from 'react-native' -import { Box, Text, HStack, VStack, Heading, Input, Button } from 'native-base' -import { connect } from 'react-redux' -import { SafeAreaView } from 'react-native-safe-area-context' -import { Formik } from 'formik' -import { useTranslation } from 'react-i18next' -import { useNavigation } from '@react-navigation/native' +import React, { useEffect, useCallback } from 'react'; +import { FlatList } from 'react-native'; +import { Box, Text, HStack, VStack, Heading, Input, Button } from 'native-base'; +import { connect } from 'react-redux'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { Formik } from 'formik'; +import { useTranslation } from 'react-i18next'; +import { useNavigation } from '@react-navigation/native'; -import { loadLoopeatFormats, updateLoopeatFormats } from '../../redux/Restaurant/actions' +import { + loadLoopeatFormats, + updateLoopeatFormats, +} from '../../redux/Restaurant/actions'; -function LoopeatFormats({ route, order, loopeatFormats, loadLoopeatFormats, updateLoopeatFormats }) { - - const { t } = useTranslation() - const navigation = useNavigation() +function LoopeatFormats({ + route, + order, + loopeatFormats, + loadLoopeatFormats, + updateLoopeatFormats, +}) { + const { t } = useTranslation(); + const navigation = useNavigation(); const fetchLoopeatFormats = useCallback(() => { - loadLoopeatFormats(order) - }, [ loadLoopeatFormats, order ]) + loadLoopeatFormats(order); + }, [loadLoopeatFormats, order]); useEffect(() => { - fetchLoopeatFormats() - }, [ fetchLoopeatFormats ]) + fetchLoopeatFormats(); + }, [fetchLoopeatFormats]); if (loopeatFormats.length === 0) { - return + return; } - const initialValues = { loopeatFormats: loopeatFormats } + const initialValues = { loopeatFormats: loopeatFormats }; return ( - - - { t('RESTAURANT_LOOPEAT_DISCLAIMER') } + + + {t('RESTAURANT_LOOPEAT_DISCLAIMER')} { - updateLoopeatFormats(order, values.loopeatFormats, (updatedOrder) => navigation.navigate('RestaurantOrder', { order: updatedOrder })) - }} - > + initialValues={initialValues} + onSubmit={values => { + updateLoopeatFormats(order, values.loopeatFormats, updatedOrder => + navigation.navigate('RestaurantOrder', { order: updatedOrder }), + ); + }}> {({ handleChange, handleBlur, handleSubmit, values }) => ( <> `loopeat_format_item_#${index}` } - renderItem={ ({ item, index }) => ( + data={values.loopeatFormats} + keyExtractor={(item, index) => `loopeat_format_item_#${index}`} + renderItem={({ item, index }) => ( - { item.orderItem.name } - { item.formats.map((format, formatIndex) => - - { format.format_name } + {item.orderItem.name} + {item.formats.map((format, formatIndex) => ( + + {format.format_name} + maxLength={2} + value={`${values.loopeatFormats[index]?.formats[formatIndex]?.quantity}`} + onChangeText={handleChange( + `loopeatFormats.${index}.formats.${formatIndex}.quantity`, + )} + /> - ) } + ))} - ) } /> + )} + /> - + )} - ) + ); } function mapStateToProps(state, ownProps) { - - const order = ownProps.route.params?.order + const order = ownProps.route.params?.order; return { order, - loopeatFormats: Object.prototype.hasOwnProperty.call(state.restaurant.loopeatFormats, order['@id']) ? - state.restaurant.loopeatFormats[order['@id']] : [], - } + loopeatFormats: Object.prototype.hasOwnProperty.call( + state.restaurant.loopeatFormats, + order['@id'], + ) + ? state.restaurant.loopeatFormats[order['@id']] + : [], + }; } function mapDispatchToProps(dispatch) { return { loadLoopeatFormats: order => dispatch(loadLoopeatFormats(order)), - updateLoopeatFormats: (order, loopeatFormats, cb) => dispatch(updateLoopeatFormats(order, loopeatFormats, cb)), - } + updateLoopeatFormats: (order, loopeatFormats, cb) => + dispatch(updateLoopeatFormats(order, loopeatFormats, cb)), + }; } -export default connect(mapStateToProps, mapDispatchToProps)(LoopeatFormats) +export default connect(mapStateToProps, mapDispatchToProps)(LoopeatFormats); diff --git a/src/navigation/restaurant/Menus.js b/src/navigation/restaurant/Menus.js index dd834f3b2..a4127eca0 100644 --- a/src/navigation/restaurant/Menus.js +++ b/src/navigation/restaurant/Menus.js @@ -1,83 +1,99 @@ -import React, { Component } from 'react' -import { FlatList, InteractionManager, StyleSheet, View } from 'react-native' -import { - Button, HStack, Icon, Pressable, Text, -} from 'native-base' -import Modal from 'react-native-modal' +import React, { Component } from 'react'; +import { FlatList, InteractionManager, StyleSheet, View } from 'react-native'; +import { Button, HStack, Icon, Pressable, Text } from 'native-base'; +import Modal from 'react-native-modal'; -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import { activateMenu, loadMenus } from '../../redux/Restaurant/actions' +import { activateMenu, loadMenus } from '../../redux/Restaurant/actions'; class Menus extends Component { - constructor(props) { - super(props) + super(props); this.state = { isModalVisible: false, currentItem: null, - } + }; } componentDidMount() { - this.props.loadMenus(this.props.restaurant) + this.props.loadMenus(this.props.restaurant); } renderItem(item) { - return ( - this.setState({ isModalVisible: true, currentItem: item }) }> + + this.setState({ isModalVisible: true, currentItem: item }) + }> - { item.name } - { item.active && } + {item.name} + {item.active && } - ) + ); } _keyExtractor(item, index) { - - return item.identifier + return item.identifier; } _onConfirm() { - const currentItem = { ...this.state.currentItem } - this.setState({ - isModalVisible: false, - currentItem: null, - }, () => { - InteractionManager.runAfterInteractions(() => - this.props.activateMenu(this.props.restaurant, currentItem)) - }) + const currentItem = { ...this.state.currentItem }; + this.setState( + { + isModalVisible: false, + currentItem: null, + }, + () => { + InteractionManager.runAfterInteractions(() => + this.props.activateMenu(this.props.restaurant, currentItem), + ); + }, + ); } render() { - - let { menus } = this.props + let { menus } = this.props; return ( this.renderItem(item) } - initialNumToRender={ 5 } /> - - - - - ) + ); } } @@ -90,20 +106,24 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, -}) +}); function mapStateToProps(state) { return { restaurant: state.restaurant.restaurant, menus: state.restaurant.menus, - } + }; } function mapDispatchToProps(dispatch) { return { - loadMenus: (restaurant) => dispatch(loadMenus(restaurant)), - activateMenu: (restaurant, menu) => dispatch(activateMenu(restaurant, menu)), - } + loadMenus: restaurant => dispatch(loadMenus(restaurant)), + activateMenu: (restaurant, menu) => + dispatch(activateMenu(restaurant, menu)), + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(Menus)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(Menus)); diff --git a/src/navigation/restaurant/OpeningHours.js b/src/navigation/restaurant/OpeningHours.js index 3082dd242..96c432538 100644 --- a/src/navigation/restaurant/OpeningHours.js +++ b/src/navigation/restaurant/OpeningHours.js @@ -1,129 +1,142 @@ -import React, { Component } from 'react' -import { FlatList } from 'react-native' -import { - Box, HStack, Heading, Icon, Pressable, Text, -} from 'native-base' -import _ from 'lodash' -import moment from 'moment' - -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' - -import { deleteOpeningHoursSpecification } from '../../redux/Restaurant/actions' -import { selectSpecialOpeningHoursSpecification } from '../../redux/Restaurant/selectors' -import FontAwesome from 'react-native-vector-icons/FontAwesome' -import ItemSeparator from '../../components/ItemSeparator' +import React, { Component } from 'react'; +import { FlatList } from 'react-native'; +import { Box, HStack, Heading, Icon, Pressable, Text } from 'native-base'; +import _ from 'lodash'; +import moment from 'moment'; -class OpeningHoursScreen extends Component { +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; + +import { deleteOpeningHoursSpecification } from '../../redux/Restaurant/actions'; +import { selectSpecialOpeningHoursSpecification } from '../../redux/Restaurant/selectors'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import ItemSeparator from '../../components/ItemSeparator'; +class OpeningHoursScreen extends Component { constructor(props) { - super(props) - this.state = { - } + super(props); + this.state = {}; } renderOpeningHours() { - const { openingHoursSpecification } = this.props + const { openingHoursSpecification } = this.props; const data = openingHoursSpecification.map(ohs => { - - let text = '' + let text = ''; const baseParams = { opens: moment(ohs.opens, 'HH:mm').format('LT'), closes: moment(ohs.closes, 'HH:mm').format('LT'), - } + }; if (ohs.dayOfWeek.length === 1) { text = this.props.t('RESTAURANT_OPENING_HOURS_ONE_DAY', { ...baseParams, day: moment().isoWeekday(ohs.dayOfWeek[0]).format('dddd'), - }) + }); } else { text = this.props.t('RESTAURANT_OPENING_HOURS_DAY_RANGE', { ...baseParams, firstDay: moment().isoWeekday(_.first(ohs.dayOfWeek)).format('dddd'), lastDay: moment().isoWeekday(_.last(ohs.dayOfWeek)).format('dddd'), - }) + }); } return { ohs, text, - } - }) + }; + }); return ( - { this.props.t('RESTAURANT_OPENING_HOURS') } + + {this.props.t('RESTAURANT_OPENING_HOURS')} + `ohs-${index}` } - renderItem={ ({ item, index }) => ( + data={data} + ItemSeparatorComponent={ItemSeparator} + keyExtractor={(item, index) => `ohs-${index}`} + renderItem={({ item, index }) => ( - { item.text } + {item.text} - )} /> + )} + /> - ) + ); } renderSpecialOpeningHours() { - const { specialOpeningHoursSpecification, httpClient } = this.props + const { specialOpeningHoursSpecification, httpClient } = this.props; return ( - { this.props.t('RESTAURANT_SPECIAL_OPENING_HOURS') } + + {this.props.t('RESTAURANT_SPECIAL_OPENING_HOURS')} + `sohs-${index}` } - renderItem={ ({ item, index }) => ( - this.props.deleteOpeningHoursSpecification(httpClient, item) }> - - { this.props.t('RESTAURANT_OPENING_HOURS_VALID_FROM_THROUGH', { - validFrom: moment(item.validFrom, 'YYYY-MM-DD').format('ll'), - validThrough: moment(item.validThrough, 'YYYY-MM-DD').format('ll') }) } + data={specialOpeningHoursSpecification} + ItemSeparatorComponent={ItemSeparator} + keyExtractor={(item, index) => `sohs-${index}`} + renderItem={({ item, index }) => ( + + this.props.deleteOpeningHoursSpecification(httpClient, item) + }> + + + {this.props.t('RESTAURANT_OPENING_HOURS_VALID_FROM_THROUGH', { + validFrom: moment(item.validFrom, 'YYYY-MM-DD').format( + 'll', + ), + validThrough: moment( + item.validThrough, + 'YYYY-MM-DD', + ).format('ll'), + })} + - )} /> + )} + /> - ) + ); } render() { - return ( - { this.renderOpeningHours() } - { this.renderSpecialOpeningHours() } + {this.renderOpeningHours()} + {this.renderSpecialOpeningHours()} - ) + ); } } function mapStateToProps(state) { - - const { restaurant } = state.restaurant + const { restaurant } = state.restaurant; return { httpClient: state.app.httpClient, openingHoursSpecification: restaurant.openingHoursSpecification, restaurant, - specialOpeningHoursSpecification: selectSpecialOpeningHoursSpecification(state), - } + specialOpeningHoursSpecification: + selectSpecialOpeningHoursSpecification(state), + }; } function mapDispatchToProps(dispatch) { return { deleteOpeningHoursSpecification: (httpClient, openingHoursSpecification) => - dispatch(deleteOpeningHoursSpecification(httpClient, openingHoursSpecification)), - } + dispatch( + deleteOpeningHoursSpecification(httpClient, openingHoursSpecification), + ), + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(OpeningHoursScreen)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(OpeningHoursScreen)); diff --git a/src/navigation/restaurant/Order.js b/src/navigation/restaurant/Order.js index 8df222a66..42c7a24a7 100644 --- a/src/navigation/restaurant/Order.js +++ b/src/navigation/restaurant/Order.js @@ -1,99 +1,123 @@ import React, { Component } from 'react'; import { View } from 'react-native'; -import { - Box, Button, HStack, - Icon, Text, -} from 'native-base'; -import { connect } from 'react-redux' -import { withTranslation } from 'react-i18next' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import { Box, Button, HStack, Icon, Text } from 'native-base'; +import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import OrderItems from '../../components/OrderItems' -import SwipeToAcceptOrRefuse from './components/SwipeToAcceptOrRefuse' -import OrderHeading from './components/OrderHeading' -import OrderAcceptedFooter from './components/OrderAcceptedFooter' +import OrderItems from '../../components/OrderItems'; +import SwipeToAcceptOrRefuse from './components/SwipeToAcceptOrRefuse'; +import OrderHeading from './components/OrderHeading'; +import OrderAcceptedFooter from './components/OrderAcceptedFooter'; -import { acceptOrder, fulfillOrder, printOrder } from '../../redux/Restaurant/actions' -import { isMultiVendor } from '../../utils/order' +import { + acceptOrder, + fulfillOrder, + printOrder, +} from '../../redux/Restaurant/actions'; +import { isMultiVendor } from '../../utils/order'; const OrderNotes = ({ order }) => { - if (order.notes) { - return ( - { order.notes } + {order.notes} - ) + ); } - return null -} + return null; +}; class OrderScreen extends Component { - fulfillOrder(order) { - this.props.fulfillOrder(order, o => this.props.navigation.setParams({ order: o })) + this.props.fulfillOrder(order, o => + this.props.navigation.setParams({ order: o }), + ); } render() { + const { order } = this.props; - const { order } = this.props - - const canEdit = !isMultiVendor(order) + const canEdit = !isMultiVendor(order); return ( this.props.navigation.navigate('RestaurantSettings', { screen: 'RestaurantPrinter' }) } - printOrder={ () => this.props.printOrder(this.props.order) } /> - - + order={order} + isPrinterConnected={this.props.isPrinterConnected} + onPrinterClick={() => + this.props.navigation.navigate('RestaurantSettings', { + screen: 'RestaurantPrinter', + }) + } + printOrder={() => this.props.printOrder(this.props.order)} + /> + + - { (order.reusablePackagingEnabled && order.restaurant.loopeatEnabled) && ( + {order.reusablePackagingEnabled && order.restaurant.loopeatEnabled && ( - - ) } - { (canEdit && order.state === 'new') && + )} + {canEdit && order.state === 'new' && ( this.props.acceptOrder(order, o => this.props.navigation.setParams({ order: o })) } - onRefuse={ () => this.props.navigation.navigate('RestaurantOrderRefuse', { order }) } /> - } - { (canEdit && order.state === 'accepted') && + onAccept={() => + this.props.acceptOrder(order, o => + this.props.navigation.setParams({ order: o }), + ) + } + onRefuse={() => + this.props.navigation.navigate('RestaurantOrderRefuse', { order }) + } + /> + )} + {canEdit && order.state === 'accepted' && ( this.props.navigation.navigate('RestaurantOrderCancel', { order }) } - onPressDelay={ () => this.props.navigation.navigate('RestaurantOrderDelay', { order }) } - onPressFulfill={ () => this.fulfillOrder(order) } /> - } + order={order} + onPressCancel={() => + this.props.navigation.navigate('RestaurantOrderCancel', { order }) + } + onPressDelay={() => + this.props.navigation.navigate('RestaurantOrderDelay', { order }) + } + onPressFulfill={() => this.fulfillOrder(order)} + /> + )} - ) + ); } } function mapStateToProps(state, ownProps) { - return { order: ownProps.route.params?.order, - isPrinterConnected: !!state.restaurant.printer || state.restaurant.isSunmiPrinter, + isPrinterConnected: + !!state.restaurant.printer || state.restaurant.isSunmiPrinter, printer: state.restaurant.printer, - } + }; } function mapDispatchToProps(dispatch) { - return { acceptOrder: (order, cb) => dispatch(acceptOrder(order, cb)), - printOrder: (order) => dispatch(printOrder(order)), + printOrder: order => dispatch(printOrder(order)), fulfillOrder: (order, cb) => dispatch(fulfillOrder(order, cb)), - } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(OrderScreen)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(OrderScreen)); diff --git a/src/navigation/restaurant/OrderCancel.js b/src/navigation/restaurant/OrderCancel.js index 44f23d3c0..b2da3db36 100644 --- a/src/navigation/restaurant/OrderCancel.js +++ b/src/navigation/restaurant/OrderCancel.js @@ -1,65 +1,87 @@ -import React, { Component } from 'react' -import { View } from 'react-native' -import { Center, Text } from 'native-base' -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' +import React, { Component } from 'react'; +import { View } from 'react-native'; +import { Center, Text } from 'native-base'; +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; -import BigButton from './components/BigButton' -import { cancelOrder } from '../../redux/Restaurant/actions' -import { resolveFulfillmentMethod } from '../../utils/order' +import BigButton from './components/BigButton'; +import { cancelOrder } from '../../redux/Restaurant/actions'; +import { resolveFulfillmentMethod } from '../../utils/order'; class OrderCancelScreen extends Component { - _cancelOrder(reason) { - this.props.cancelOrder( - this.props.route.params?.order, - reason, - order => this.props.navigation.navigate('RestaurantOrder', { order }) - ) + this.props.cancelOrder(this.props.route.params?.order, reason, order => + this.props.navigation.navigate('RestaurantOrder', { order }), + ); } render() { - - const order = this.props.route.params?.order - const fulfillmentMethod = resolveFulfillmentMethod(order) + const order = this.props.route.params?.order; + const fulfillmentMethod = resolveFulfillmentMethod(order); return ( -
+
- - { this.props.t('RESTAURANT_ORDER_CANCEL_DISCLAIMER') } - + {this.props.t('RESTAURANT_ORDER_CANCEL_DISCLAIMER')} this._cancelOrder('CUSTOMER') } /> - this._cancelOrder('SOLD_OUT') } /> + heading={this.props.t( + 'RESTAURANT_ORDER_CANCEL_REASON_CUSTOMER_HEADING', + )} + text={`${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_WILL_BE_REFUSED', + )}\n${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_CONTINUE_RECEIVING', + )}`} + onPress={() => this._cancelOrder('CUSTOMER')} + /> this._cancelOrder('RUSH_HOUR') } /> - { fulfillmentMethod === 'collection' && ( + heading={this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_SOLD_OUT_HEADING', + )} + text={`${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_WILL_BE_REFUSED', + )}\n${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_CONTINUE_RECEIVING', + )}`} + onPress={() => this._cancelOrder('SOLD_OUT')} + /> this._cancelOrder('NO_SHOW') } />) } + heading={this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_RUSH_HOUR_HEADING', + )} + text={`${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_WILL_BE_REFUSED', + )}\n${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_CONTINUE_RECEIVING', + )}`} + onPress={() => this._cancelOrder('RUSH_HOUR')} + /> + {fulfillmentMethod === 'collection' && ( + this._cancelOrder('NO_SHOW')} + /> + )}
- ) + ); } } function mapDispatchToProps(dispatch) { - return { - cancelOrder: (order, reason, cb) => dispatch(cancelOrder(order, reason, cb)), - } + cancelOrder: (order, reason, cb) => + dispatch(cancelOrder(order, reason, cb)), + }; } -export default connect(() => ({}), mapDispatchToProps)(withTranslation()(OrderCancelScreen)) +export default connect( + () => ({}), + mapDispatchToProps, +)(withTranslation()(OrderCancelScreen)); diff --git a/src/navigation/restaurant/OrderDelay.js b/src/navigation/restaurant/OrderDelay.js index bc64774d8..c0271a117 100644 --- a/src/navigation/restaurant/OrderDelay.js +++ b/src/navigation/restaurant/OrderDelay.js @@ -1,51 +1,52 @@ -import React, { Component } from 'react' -import { View } from 'react-native' -import { Center, Text } from 'native-base' -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' +import React, { Component } from 'react'; +import { View } from 'react-native'; +import { Center, Text } from 'native-base'; +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; -import BigButton from './components/BigButton' -import { delayOrder } from '../../redux/Restaurant/actions' +import BigButton from './components/BigButton'; +import { delayOrder } from '../../redux/Restaurant/actions'; class OrderDelayScreen extends Component { - _delayOrder(delay) { - this.props.delayOrder( - this.props.route.params?.order, - delay, - order => this.props.navigation.navigate('RestaurantOrder', { order }) - ) + this.props.delayOrder(this.props.route.params?.order, delay, order => + this.props.navigation.navigate('RestaurantOrder', { order }), + ); } render() { return ( -
+
- - { this.props.t('RESTAURANT_ORDER_DELAY_DISCLAIMER') } - + {this.props.t('RESTAURANT_ORDER_DELAY_DISCLAIMER')} this._delayOrder(10) } /> + heading={'10 minutes'} + onPress={() => this._delayOrder(10)} + /> + this._delayOrder(20)} + /> this._delayOrder(20) } /> - this._delayOrder(30) } /> + danger + heading={'30 minutes'} + onPress={() => this._delayOrder(30)} + />
- ) + ); } } function mapDispatchToProps(dispatch) { - return { delayOrder: (order, delay, cb) => dispatch(delayOrder(order, delay, cb)), - } + }; } -export default connect(() => ({}), mapDispatchToProps)(withTranslation()(OrderDelayScreen)) +export default connect( + () => ({}), + mapDispatchToProps, +)(withTranslation()(OrderDelayScreen)); diff --git a/src/navigation/restaurant/OrderRefuse.js b/src/navigation/restaurant/OrderRefuse.js index 305155af3..dbef3d8fb 100644 --- a/src/navigation/restaurant/OrderRefuse.js +++ b/src/navigation/restaurant/OrderRefuse.js @@ -1,60 +1,78 @@ -import React, { Component } from 'react' -import { View } from 'react-native' -import { Center, Text } from 'native-base' -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' +import React, { Component } from 'react'; +import { View } from 'react-native'; +import { Center, Text } from 'native-base'; +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; -import BigButton from './components/BigButton' -import { refuseOrder } from '../../redux/Restaurant/actions' +import BigButton from './components/BigButton'; +import { refuseOrder } from '../../redux/Restaurant/actions'; class OrderRefuseScreen extends Component { - _refuseOrder(reason) { - this.props.refuseOrder( - this.props.route.params?.order, - reason, - order => this.props.navigation.navigate('RestaurantOrder', { order }) - ) + this.props.refuseOrder(this.props.route.params?.order, reason, order => + this.props.navigation.navigate('RestaurantOrder', { order }), + ); } render() { - return ( -
+
- - { this.props.t('RESTAURANT_ORDER_REFUSE_DISCLAIMER') } - + {this.props.t('RESTAURANT_ORDER_REFUSE_DISCLAIMER')} this._refuseOrder('SOLD_OUT') } /> + heading={this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_SOLD_OUT_HEADING', + )} + text={`${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_WILL_BE_REFUSED', + )}\n${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_CONTINUE_RECEIVING', + )}`} + onPress={() => this._refuseOrder('SOLD_OUT')} + /> + this._refuseOrder('RUSH_HOUR')} + /> this._refuseOrder('RUSH_HOUR') } /> - this._refuseOrder('CLOSING') } /> + danger + heading={this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_CLOSING_HEADING', + )} + text={`${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_WILL_BE_REFUSED', + )}\n${this.props.t( + 'RESTAURANT_ORDER_REFUSE_REASON_ORDER_STOP_RECEIVING', + )}`} + onPress={() => this._refuseOrder('CLOSING')} + />
- ) + ); } } function mapStateToProps(state) { - - return {} + return {}; } function mapDispatchToProps(dispatch) { - return { - refuseOrder: (order, reason, cb) => dispatch(refuseOrder(order, reason, cb)), - } + refuseOrder: (order, reason, cb) => + dispatch(refuseOrder(order, reason, cb)), + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(OrderRefuseScreen)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(OrderRefuseScreen)); diff --git a/src/navigation/restaurant/Printer.js b/src/navigation/restaurant/Printer.js index 0bbadecb5..af603b6c3 100644 --- a/src/navigation/restaurant/Printer.js +++ b/src/navigation/restaurant/Printer.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React, { Component } from 'react'; import { ActivityIndicator, FlatList, @@ -8,124 +8,149 @@ import { Platform, StyleSheet, TouchableOpacity, -} from 'react-native' -import { Center, Icon, Text } from 'native-base' -import { withTranslation } from 'react-i18next' -import _ from 'lodash' -import { connect } from 'react-redux' -import BleManager from 'react-native-ble-manager' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +} from 'react-native'; +import { Center, Icon, Text } from 'native-base'; +import { withTranslation } from 'react-i18next'; +import _ from 'lodash'; +import { connect } from 'react-redux'; +import BleManager from 'react-native-ble-manager'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import { bluetoothStartScan, connectPrinter, disconnectPrinter } from '../../redux/Restaurant/actions' -import ItemSeparator from '../../components/ItemSeparator' -import { getMissingAndroidPermissions } from '../../utils/bluetooth' +import { + bluetoothStartScan, + connectPrinter, + disconnectPrinter, +} from '../../redux/Restaurant/actions'; +import ItemSeparator from '../../components/ItemSeparator'; +import { getMissingAndroidPermissions } from '../../utils/bluetooth'; -const BleManagerModule = NativeModules.BleManager -const bleManagerEmitter = new NativeEventEmitter(BleManagerModule) +const BleManagerModule = NativeModules.BleManager; +const bleManagerEmitter = new NativeEventEmitter(BleManagerModule); class Printer extends Component { - constructor(props) { - super(props) + super(props); this.state = { devices: [], - } + }; } componentDidMount() { - this.discoverPeripheral = bleManagerEmitter.addListener('BleManagerDiscoverPeripheral', (device) => { - - const devices = this.state.devices.slice(0) - devices.push(device) - - this.setState({ devices: _.uniqBy(devices, 'id') }) - }) + this.discoverPeripheral = bleManagerEmitter.addListener( + 'BleManagerDiscoverPeripheral', + device => { + const devices = this.state.devices.slice(0); + devices.push(device); + + this.setState({ devices: _.uniqBy(devices, 'id') }); + }, + ); } componentWillUnmount() { - this.discoverPeripheral.remove() + this.discoverPeripheral.remove(); } _connect(device) { - this.props.connectPrinter(device, () => this.props.navigation.navigate('RestaurantSettings')) + this.props.connectPrinter(device, () => + this.props.navigation.navigate('RestaurantSettings'), + ); } _disconnect(device) { - this.props.disconnectPrinter(device, () => this.props.navigation.navigate('RestaurantSettings')) + this.props.disconnectPrinter(device, () => + this.props.navigation.navigate('RestaurantSettings'), + ); } async _onPressScan() { - if (this.props.isScanning) { - return + return; } if (Platform.OS === 'android') { - - const missingPermissions = await getMissingAndroidPermissions() + const missingPermissions = await getMissingAndroidPermissions(); if (missingPermissions.length > 0) { - const granted = await PermissionsAndroid.requestMultiple(missingPermissions) - const allPermissionsGranted = _.values(granted).every(value => value === PermissionsAndroid.RESULTS.GRANTED) + const granted = await PermissionsAndroid.requestMultiple( + missingPermissions, + ); + const allPermissionsGranted = _.values(granted).every( + value => value === PermissionsAndroid.RESULTS.GRANTED, + ); if (allPermissionsGranted) { - this.props.bluetoothStartScan() + this.props.bluetoothStartScan(); } } else { - this.props.bluetoothStartScan() + this.props.bluetoothStartScan(); } - } else { - this.props.bluetoothStartScan() + this.props.bluetoothStartScan(); } } renderItem(item) { - return ( - item.isConnected ? this._disconnect(item) : this._connect(item) }> + + item.isConnected ? this._disconnect(item) : this._connect(item) + }> - { item.name || (item.advertising && item.advertising.localName) || item.id } + {item.name || + (item.advertising && item.advertising.localName) || + item.id} - + - ) + ); } render() { + const { devices } = this.state; + const { isScanning, printer } = this.props; - const { devices } = this.state - const { isScanning, printer } = this.props - - let items = [] + let items = []; if (printer) { - items.push({ ...printer, isConnected: true }) + items.push({ ...printer, isConnected: true }); } else { - items = devices.map(device => ({ ...device, isConnected: false })) + items = devices.map(device => ({ ...device, isConnected: false })); } - const hasItems = !isScanning && items.length > 0 + const hasItems = !isScanning && items.length > 0; if (!hasItems) { - return ( -
- this._onPressScan() } style={{ padding: 15, alignItems: 'center' }}> - - { this.props.t('SCAN_FOR_PRINTERS') } - { isScanning && } +
+ this._onPressScan()} + style={{ padding: 15, alignItems: 'center' }}> + + {this.props.t('SCAN_FOR_PRINTERS')} + {isScanning && ( + + )}
- ) + ); } return ( item.id } - renderItem={ ({ item }) => this.renderItem(item) } - ItemSeparatorComponent={ ItemSeparator } /> - ) + data={items} + keyExtractor={item => item.id} + renderItem={({ item }) => this.renderItem(item)} + ItemSeparatorComponent={ItemSeparator} + /> + ); } } @@ -140,23 +165,24 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'space-between', }, -}) +}); function mapStateToProps(state) { - return { isScanning: state.restaurant.isScanningBluetooth, printer: state.restaurant.printer, - } + }; } function mapDispatchToProps(dispatch) { - return { connectPrinter: (device, cb) => dispatch(connectPrinter(device, cb)), disconnectPrinter: (device, cb) => dispatch(disconnectPrinter(device, cb)), bluetoothStartScan: () => dispatch(bluetoothStartScan()), - } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(Printer)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(Printer)); diff --git a/src/navigation/restaurant/ProductOptions.js b/src/navigation/restaurant/ProductOptions.js index 82564d32a..fe3c5eb56 100644 --- a/src/navigation/restaurant/ProductOptions.js +++ b/src/navigation/restaurant/ProductOptions.js @@ -1,76 +1,80 @@ -import React, { Component } from 'react' -import { SectionList, View } from 'react-native' -import { - HStack, Heading, Switch, Text, -} from 'native-base' -import _ from 'lodash' +import React, { Component } from 'react'; +import { SectionList, View } from 'react-native'; +import { HStack, Heading, Switch, Text } from 'native-base'; +import _ from 'lodash'; -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; -import { changeProductOptionValueEnabled, loadProductOptions } from '../../redux/Restaurant/actions' -import ItemSeparator from '../../components/ItemSeparator' +import { + changeProductOptionValueEnabled, + loadProductOptions, +} from '../../redux/Restaurant/actions'; +import ItemSeparator from '../../components/ItemSeparator'; -const SectionHeader = ({ section }) => ( - { section.title } -) +const SectionHeader = ({ section }) => {section.title}; class ProductOptions extends Component { - componentDidMount() { - this.props.loadProductOptions(this.props.restaurant) + this.props.loadProductOptions(this.props.restaurant); } _toggleProductEnabled(product, enabled) { - this.props.changeProductOptionValueEnabled(product, enabled) + this.props.changeProductOptionValueEnabled(product, enabled); } _renderItem(productOptionValue) { - return ( - { productOptionValue.value } + {productOptionValue.value} this._toggleProductEnabled(productOptionValue, value) } /> + isChecked={productOptionValue.enabled} + onToggle={value => + this._toggleProductEnabled(productOptionValue, value) + } + /> ); } render() { - - const sections = _.map(this.props.productOptions, (productOption) => ({ + const sections = _.map(this.props.productOptions, productOption => ({ title: productOption.name, data: productOption.values, - })) + })); return ( this._renderItem(item) } - renderSectionHeader={ SectionHeader } - ItemSeparatorComponent={ ItemSeparator } - keyExtractor={ (item, index) => index } + sections={sections} + renderItem={({ item }) => this._renderItem(item)} + renderSectionHeader={SectionHeader} + ItemSeparatorComponent={ItemSeparator} + keyExtractor={(item, index) => index} /> - ) + ); } } function mapStateToProps(state) { return { restaurant: state.restaurant.restaurant, - productOptions: state.restaurant.productOptions.sort((a, b) => a.name < b.name ? -1 : 1), - } + productOptions: state.restaurant.productOptions.sort((a, b) => + a.name < b.name ? -1 : 1, + ), + }; } function mapDispatchToProps(dispatch) { - return { - loadProductOptions: (restaurant) => dispatch(loadProductOptions(restaurant)), - changeProductOptionValueEnabled: (productOptionValue, enabled) => dispatch(changeProductOptionValueEnabled(productOptionValue, enabled)), - } + loadProductOptions: restaurant => dispatch(loadProductOptions(restaurant)), + changeProductOptionValueEnabled: (productOptionValue, enabled) => + dispatch(changeProductOptionValueEnabled(productOptionValue, enabled)), + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ProductOptions)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(ProductOptions)); diff --git a/src/navigation/restaurant/Products.js b/src/navigation/restaurant/Products.js index 2c58c8904..7bb9b519e 100644 --- a/src/navigation/restaurant/Products.js +++ b/src/navigation/restaurant/Products.js @@ -1,71 +1,71 @@ -import React, { Component } from 'react' -import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native' -import { - HStack, Switch, Text, -} from 'native-base' +import React, { Component } from 'react'; +import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { HStack, Switch, Text } from 'native-base'; -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; -import { changeProductEnabled, loadMoreProducts, loadProducts } from '../../redux/Restaurant/actions' -import ItemSeparator from '../../components/ItemSeparator' +import { + changeProductEnabled, + loadMoreProducts, + loadProducts, +} from '../../redux/Restaurant/actions'; +import ItemSeparator from '../../components/ItemSeparator'; class ProductsScreen extends Component { - componentDidMount() { - this.props.loadProducts(this.props.httpClient, this.props.restaurant) + this.props.loadProducts(this.props.httpClient, this.props.restaurant); } _toggleProductEnabled(product, value) { - this.props.changeProductEnabled(this.props.httpClient, product, value) + this.props.changeProductEnabled(this.props.httpClient, product, value); } renderItem(item) { return ( - { item.name } + {item.name} + isChecked={item.enabled} + onToggle={this._toggleProductEnabled.bind(this, item)} + /> - ) + ); } _keyExtractor(item, index) { - - return item['@id'] + return item['@id']; } render() { - - const { products, hasMoreProducts } = this.props + const { products, hasMoreProducts } = this.props; return ( this.renderItem(item) } - initialNumToRender={ 15 } - ItemSeparatorComponent={ ItemSeparator } - ListFooterComponent={ () => { - + data={products} + keyExtractor={this._keyExtractor} + renderItem={({ item }) => this.renderItem(item)} + initialNumToRender={15} + ItemSeparatorComponent={ItemSeparator} + ListFooterComponent={() => { if (products.length > 0 && hasMoreProducts) { return ( this.props.loadMoreProducts() } - style={ styles.btn }> - { this.props.t('LOAD_MORE') } + onPress={() => this.props.loadMoreProducts()} + style={styles.btn}> + + {this.props.t('LOAD_MORE')} + - ) + ); } - return ( - - ) - }} /> + return ; + }} + /> - ) + ); } } @@ -79,23 +79,30 @@ const styles = StyleSheet.create({ btnText: { color: '#0074D9', }, -}) +}); function mapStateToProps(state) { return { httpClient: state.app.httpClient, restaurant: state.restaurant.restaurant, - products: state.restaurant.products.sort((a, b) => a.name < b.name ? -1 : 1), + products: state.restaurant.products.sort((a, b) => + a.name < b.name ? -1 : 1, + ), hasMoreProducts: state.restaurant.hasMoreProducts, - } + }; } function mapDispatchToProps(dispatch) { return { - loadProducts: (httpClient, restaurant) => dispatch(loadProducts(httpClient, restaurant)), + loadProducts: (httpClient, restaurant) => + dispatch(loadProducts(httpClient, restaurant)), loadMoreProducts: () => dispatch(loadMoreProducts()), - changeProductEnabled: (httpClient, product, enabled) => dispatch(changeProductEnabled(httpClient, product, enabled)), - } + changeProductEnabled: (httpClient, product, enabled) => + dispatch(changeProductEnabled(httpClient, product, enabled)), + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ProductsScreen)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(ProductsScreen)); diff --git a/src/navigation/restaurant/Settings.js b/src/navigation/restaurant/Settings.js index bcec337d5..438e1a67d 100644 --- a/src/navigation/restaurant/Settings.js +++ b/src/navigation/restaurant/Settings.js @@ -1,38 +1,45 @@ -import React, { Component } from 'react' -import { View } from 'react-native' +import React, { Component } from 'react'; +import { View } from 'react-native'; import { - Box, FlatList, HStack, Icon, Pressable, Switch, Text, -} from 'native-base' -import FontAwesome from 'react-native-vector-icons/FontAwesome' -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' - -import { changeStatus } from '../../redux/Restaurant/actions' -import ItemSeparator from '../../components/ItemSeparator' + Box, + FlatList, + HStack, + Icon, + Pressable, + Switch, + Text, +} from 'native-base'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; + +import { changeStatus } from '../../redux/Restaurant/actions'; +import ItemSeparator from '../../components/ItemSeparator'; class SettingsScreen extends Component { - constructor(props) { - super(props) + super(props); this.state = { restaurantState: props.restaurant.state, - } + }; } componentDidUpdate(prevProps, prevState) { if (this.state.restaurantState !== prevState.restaurantState) { - this.props.changeStatus(this.props.restaurant, this.state.restaurantState) + this.props.changeStatus( + this.props.restaurant, + this.state.restaurantState, + ); } } _onRushValueChange(value) { - this.setState({ restaurantState: value ? 'rush' : 'normal' }) + this.setState({ restaurantState: value ? 'rush' : 'normal' }); } render() { - - const { navigate } = this.props.navigation - const { restaurants } = this.props + const { navigate } = this.props.navigation; + const { restaurants } = this.props; const items = [ { @@ -40,8 +47,9 @@ class SettingsScreen extends Component { label: this.props.t('RESTAURANT_SETTINGS_RUSH'), switch: ( + isChecked={this.state.restaurantState === 'rush'} + onToggle={this._onRushValueChange.bind(this)} + /> ), }, { @@ -69,55 +77,54 @@ class SettingsScreen extends Component { label: this.props.t('RESTAURANT_SETTINGS_PRINTER'), onPress: () => navigate('RestaurantPrinter'), }, - ] + ]; if (restaurants.length > 1) { - items.push( - { - icon: 'refresh', - label: this.props.t('RESTAURANT_SETTINGS_CHANGE_RESTAURANT'), - onPress: () => navigate('RestaurantList'), - } - - ) + items.push({ + icon: 'refresh', + label: this.props.t('RESTAURANT_SETTINGS_CHANGE_RESTAURANT'), + onPress: () => navigate('RestaurantList'), + }); } return ( - { this.props.t('RESTAURANT_SETTINGS_HEADING', { name: this.props.restaurant.name }) } + {this.props.t('RESTAURANT_SETTINGS_HEADING', { + name: this.props.restaurant.name, + })} `item-${index}` } - renderItem={ ({ item }) => { - - let itemProps = {} + data={items} + keyExtractor={(item, index) => `item-${index}`} + renderItem={({ item }) => { + let itemProps = {}; if (item.onPress) { itemProps = { ...itemProps, onPress: item.onPress, - } + }; } return ( - + - - { item.label } + + {item.label} - { item.switch && item.switch } + {item.switch && item.switch} - ) + ); }} - ItemSeparatorComponent={ ItemSeparator } /> + ItemSeparatorComponent={ItemSeparator} + /> - ) + ); } } @@ -126,13 +133,17 @@ function mapStateToProps(state) { httpClient: state.app.httpClient, restaurant: state.restaurant.restaurant, restaurants: state.restaurant.myRestaurants, - } + }; } function mapDispatchToProps(dispatch) { return { - changeStatus: (restaurant, state) => dispatch(changeStatus(restaurant, state)), - } + changeStatus: (restaurant, state) => + dispatch(changeStatus(restaurant, state)), + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(SettingsScreen)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(SettingsScreen)); diff --git a/src/navigation/restaurant/SettingsNavigator.js b/src/navigation/restaurant/SettingsNavigator.js index a52e6413f..c10b185bc 100644 --- a/src/navigation/restaurant/SettingsNavigator.js +++ b/src/navigation/restaurant/SettingsNavigator.js @@ -1,57 +1,56 @@ -import React from 'react' -import { createStackNavigator } from '@react-navigation/stack' +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; -import i18n from '../../i18n' -import screens from '..' -import { stackNavigatorScreenOptions } from '../styles' -import ProductOptions from './ProductOptions' +import i18n from '../../i18n'; +import screens from '..'; +import { stackNavigatorScreenOptions } from '../styles'; +import ProductOptions from './ProductOptions'; -const RootStack = createStackNavigator() +const RootStack = createStackNavigator(); export default () => ( - + -) +); diff --git a/src/navigation/restaurant/components/BigButton.js b/src/navigation/restaurant/components/BigButton.js index d87362580..8feeaf439 100644 --- a/src/navigation/restaurant/components/BigButton.js +++ b/src/navigation/restaurant/components/BigButton.js @@ -1,38 +1,38 @@ -import React from 'react' -import { StyleSheet, TouchableOpacity, View } from 'react-native' -import { Icon, Text } from 'native-base' -import FontAwesome from 'react-native-vector-icons/FontAwesome' -import material from '../../../../native-base-theme/variables/material' +import React from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { Icon, Text } from 'native-base'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import material from '../../../../native-base-theme/variables/material'; export default ({ heading, text, onPress, danger }) => { - - const btnStyles = [styles.btn] - const btnTextHeadingStyles = [styles.btnTextHeading] - const btnTextNoteStyles = [] + const btnStyles = [styles.btn]; + const btnTextHeadingStyles = [styles.btnTextHeading]; + const btnTextNoteStyles = []; if (danger) { - btnStyles.push(styles.btnDanger) - btnTextHeadingStyles.push(styles.textDanger) - btnTextNoteStyles.push(styles.textDanger) + btnStyles.push(styles.btnDanger); + btnTextHeadingStyles.push(styles.textDanger); + btnTextNoteStyles.push(styles.textDanger); } - const iconColor = danger ? material.brandDanger : '#ccc' + const iconColor = danger ? material.brandDanger : '#ccc'; return ( - + - - { heading } - - - { text } + {heading} + + {text} - + - ) - -} + ); +}; const styles = StyleSheet.create({ btn: { @@ -55,4 +55,4 @@ const styles = StyleSheet.create({ textDanger: { color: material.brandDanger, }, -}) +}); diff --git a/src/navigation/restaurant/components/DatePickerHeader.js b/src/navigation/restaurant/components/DatePickerHeader.js index e8f8583b3..ae2fed695 100644 --- a/src/navigation/restaurant/components/DatePickerHeader.js +++ b/src/navigation/restaurant/components/DatePickerHeader.js @@ -1,39 +1,49 @@ -import React, { Component } from 'react' -import { Dimensions } from 'react-native' -import { HStack, Icon, Pressable, Text } from 'native-base' -import { withTranslation } from 'react-i18next' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import React, { Component } from 'react'; +import { Dimensions } from 'react-native'; +import { HStack, Icon, Pressable, Text } from 'native-base'; +import { withTranslation } from 'react-i18next'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; class DatePickerHeader extends Component { - render() { + const { width } = Dimensions.get('window'); + const { date } = this.props; - const { width } = Dimensions.get('window') - const { date } = this.props - - let dateFormat = 'L' + let dateFormat = 'L'; if (width > 400) { - dateFormat = 'dddd LL' + dateFormat = 'dddd LL'; } return ( - this.props.onCalendarClick() }> - + this.props.onCalendarClick()}> + - { date.format(dateFormat) } - + {date.format(dateFormat)} + - this.props.onTodayClick() }> - + this.props.onTodayClick()}> + - { this.props.t('TODAY') } + {this.props.t('TODAY')} - ) + ); } } -export default withTranslation()(DatePickerHeader) +export default withTranslation()(DatePickerHeader); diff --git a/src/navigation/restaurant/components/HeaderRight.js b/src/navigation/restaurant/components/HeaderRight.js index 76c2ef3b6..5a5e2624d 100644 --- a/src/navigation/restaurant/components/HeaderRight.js +++ b/src/navigation/restaurant/components/HeaderRight.js @@ -1,19 +1,22 @@ -import React, { Component } from 'react' -import { Alert } from 'react-native' -import { withTranslation } from 'react-i18next' -import { connect } from 'react-redux' -import { HeaderButton, HeaderButtons, Item } from 'react-navigation-header-buttons' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import React, { Component } from 'react'; +import { Alert } from 'react-native'; +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import { + HeaderButton, + HeaderButtons, + Item, +} from 'react-navigation-header-buttons'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import { closeRestaurant } from '../../../redux/Restaurant/actions' -import { selectSpecialOpeningHoursSpecificationForToday } from '../../../redux/Restaurant/selectors' +import { closeRestaurant } from '../../../redux/Restaurant/actions'; +import { selectSpecialOpeningHoursSpecificationForToday } from '../../../redux/Restaurant/selectors'; const FontAwesomeHeaderButton = props => ( - -) + +); class HeaderRight extends Component { - onPressClose() { Alert.alert( this.props.t('RESTAURANT_CLOSE_ALERT_TITLE'), @@ -27,39 +30,48 @@ class HeaderRight extends Component { text: this.props.t('CANCEL'), style: 'cancel', }, - ] - ) + ], + ); } render() { - - const { navigate } = this.props.navigation - const { specialOpeningHoursSpecification } = this.props + const { navigate } = this.props.navigation; + const { specialOpeningHoursSpecification } = this.props; return ( - - { !specialOpeningHoursSpecification && ( - this.onPressClose() } /> + + {!specialOpeningHoursSpecification && ( + this.onPressClose()} + /> )} - navigate('RestaurantSettings') } /> + navigate('RestaurantSettings')} + /> - ) + ); } } function mapStateToProps(state) { - return { restaurant: state.restaurant.restaurant, - specialOpeningHoursSpecification: selectSpecialOpeningHoursSpecificationForToday(state), - } + specialOpeningHoursSpecification: + selectSpecialOpeningHoursSpecificationForToday(state), + }; } function mapDispatchToProps(dispatch) { - return { closeRestaurant: restaurant => dispatch(closeRestaurant(restaurant)), - } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(HeaderRight)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(HeaderRight)); diff --git a/src/navigation/restaurant/components/OrderAcceptedFooter.js b/src/navigation/restaurant/components/OrderAcceptedFooter.js index 4d814e205..d5d01958a 100644 --- a/src/navigation/restaurant/components/OrderAcceptedFooter.js +++ b/src/navigation/restaurant/components/OrderAcceptedFooter.js @@ -1,13 +1,10 @@ import React from 'react'; import { StyleSheet, TouchableOpacity } from 'react-native'; -import { useTranslation } from 'react-i18next' -import { - HStack, - Text, -} from 'native-base'; +import { useTranslation } from 'react-i18next'; +import { HStack, Text } from 'native-base'; -import { resolveFulfillmentMethod } from '../../../utils/order' -import material from '../../../../native-base-theme/variables/material' +import { resolveFulfillmentMethod } from '../../../utils/order'; +import material from '../../../../native-base-theme/variables/material'; const styles = StyleSheet.create({ footerBtn: { @@ -41,39 +38,43 @@ const styles = StyleSheet.create({ }, }); -const OrderAcceptedFooter = ({ order, onPressCancel, onPressDelay, onPressFulfill }) => { +const OrderAcceptedFooter = ({ + order, + onPressCancel, + onPressDelay, + onPressFulfill, +}) => { + const { t } = useTranslation(); - const { t } = useTranslation() - - const fulfillmentMethod = resolveFulfillmentMethod(order) + const fulfillmentMethod = resolveFulfillmentMethod(order); return ( - - { t('RESTAURANT_ORDER_BUTTON_CANCEL') } - - - - - { t('RESTAURANT_ORDER_BUTTON_DELAY') } + style={[styles.footerBtn, styles.refuseBtn]} + onPress={onPressCancel}> + + {t('RESTAURANT_ORDER_BUTTON_CANCEL')} - { fulfillmentMethod === 'collection' && ( - - { t('RESTAURANT_ORDER_BUTTON_FULFILL') } + style={[styles.footerBtn, styles.delayBtn]} + onPress={onPressDelay}> + + {t('RESTAURANT_ORDER_BUTTON_DELAY')} + {fulfillmentMethod === 'collection' && ( + + + {t('RESTAURANT_ORDER_BUTTON_FULFILL')} + + )} - ) -} + ); +}; -export default OrderAcceptedFooter +export default OrderAcceptedFooter; diff --git a/src/navigation/restaurant/components/OrderButtons.js b/src/navigation/restaurant/components/OrderButtons.js index 761742783..431feea8d 100644 --- a/src/navigation/restaurant/components/OrderButtons.js +++ b/src/navigation/restaurant/components/OrderButtons.js @@ -1,51 +1,54 @@ -import React from 'react' -import { View } from 'react-native' -import { useTranslation } from 'react-i18next' -import { PhoneNumberFormat, PhoneNumberUtil } from 'google-libphonenumber' -import { Button, Icon } from 'native-base' -import { phonecall } from 'react-native-communications' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import React from 'react'; +import { View } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import { PhoneNumberFormat, PhoneNumberUtil } from 'google-libphonenumber'; +import { Button, Icon } from 'native-base'; +import { phonecall } from 'react-native-communications'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; -const phoneNumberUtil = PhoneNumberUtil.getInstance() +const phoneNumberUtil = PhoneNumberUtil.getInstance(); const Comp = ({ order, isPrinterConnected, onPrinterClick, printOrder }) => { + const { t } = useTranslation(); - const { t } = useTranslation() - - let phoneNumber - let isPhoneValid = false + let phoneNumber; + let isPhoneValid = false; try { - phoneNumber = phoneNumberUtil.parse(order.customer.telephone) - isPhoneValid = true + phoneNumber = phoneNumberUtil.parse(order.customer.telephone); + isPhoneValid = true; } catch (e) {} return ( - { isPrinterConnected && ( - + {isPrinterConnected && ( + )} - { !isPrinterConnected && ( - + {!isPrinterConnected && ( + )} - { isPhoneValid && ( - + {isPhoneValid && ( + )} - ) -} + ); +}; -export default Comp +export default Comp; diff --git a/src/navigation/restaurant/components/OrderHeading.js b/src/navigation/restaurant/components/OrderHeading.js index 39d0b88e9..daad6b510 100644 --- a/src/navigation/restaurant/components/OrderHeading.js +++ b/src/navigation/restaurant/components/OrderHeading.js @@ -1,17 +1,17 @@ -import React from 'react' -import { StyleSheet, View } from 'react-native' -import { useTranslation } from 'react-i18next' -import moment from 'moment' -import { HStack, Icon, Text } from 'native-base' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import moment from 'moment'; +import { HStack, Icon, Text } from 'native-base'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import material from '../../../../native-base-theme/variables/material' -import OrderButtons from './OrderButtons' -import { resolveFulfillmentMethod } from '../../../utils/order' -import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon' -import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo' +import material from '../../../../native-base-theme/variables/material'; +import OrderButtons from './OrderButtons'; +import { resolveFulfillmentMethod } from '../../../utils/order'; +import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon'; +import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo'; -const fallbackFormat = 'dddd D MMM' +const fallbackFormat = 'dddd D MMM'; const styles = StyleSheet.create({ fulfillment: { @@ -26,44 +26,73 @@ const styles = StyleSheet.create({ }, }); -const OrderHeading = ({ order, isPrinterConnected, onPrinterClick, printOrder }) => { +const OrderHeading = ({ + order, + isPrinterConnected, + onPrinterClick, + printOrder, +}) => { + const { t } = useTranslation(); - const { t } = useTranslation() - - const preparationExpectedAt = moment.parseZone(order.preparationExpectedAt) - const pickupExpectedAt = moment.parseZone(order.pickupExpectedAt) + const preparationExpectedAt = moment.parseZone(order.preparationExpectedAt); + const pickupExpectedAt = moment.parseZone(order.pickupExpectedAt); return ( - - - - { moment(pickupExpectedAt).calendar(null, { - lastDay : fallbackFormat, - sameDay: `[${t('TODAY')}]`, - nextDay: `[${t('TOMORROW')}]`, - lastWeek : fallbackFormat, - nextWeek : fallbackFormat, - sameElse : fallbackFormat, - }) } - { t(`FULFILLMENT_METHOD.${resolveFulfillmentMethod(order)}`) } + + + + + {moment(pickupExpectedAt).calendar(null, { + lastDay: fallbackFormat, + sameDay: `[${t('TODAY')}]`, + nextDay: `[${t('TOMORROW')}]`, + lastWeek: fallbackFormat, + nextWeek: fallbackFormat, + sameElse: fallbackFormat, + })} + + + {t(`FULFILLMENT_METHOD.${resolveFulfillmentMethod(order)}`)} + - + - { t('RESTAURANT_ORDER_PREPARATION_EXPECTED_AT', { date: preparationExpectedAt.format('LT') }) } - { t('RESTAURANT_ORDER_PICKUP_EXPECTED_AT', { date: pickupExpectedAt.format('LT') }) } + + {t('RESTAURANT_ORDER_PREPARATION_EXPECTED_AT', { + date: preparationExpectedAt.format('LT'), + })} + + + {t('RESTAURANT_ORDER_PICKUP_EXPECTED_AT', { + date: pickupExpectedAt.format('LT'), + })} + - + + order={order} + isPrinterConnected={isPrinterConnected} + onPrinterClick={onPrinterClick} + printOrder={printOrder} + /> - ) -} + ); +}; -export default OrderHeading +export default OrderHeading; diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index 9a6293308..3683453a9 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -1,16 +1,16 @@ -import React, { Component } from 'react' -import { SectionList, StyleSheet, TouchableOpacity, View } from 'react-native' -import { HStack, Icon, Text } from 'native-base' -import moment from 'moment' -import { withTranslation } from 'react-i18next' -import Ionicons from 'react-native-vector-icons/Ionicons' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import React, { Component } from 'react'; +import { SectionList, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { HStack, Icon, Text } from 'native-base'; +import moment from 'moment'; +import { withTranslation } from 'react-i18next'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import { formatPrice } from '../../../utils/formatting' -import OrderNumber from '../../../components/OrderNumber' -import ItemSeparatorComponent from '../../../components/ItemSeparator' -import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon' -import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo' +import { formatPrice } from '../../../utils/formatting'; +import OrderNumber from '../../../components/OrderNumber'; +import ItemSeparatorComponent from '../../../components/ItemSeparator'; +import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon'; +import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo'; const styles = StyleSheet.create({ item: { @@ -28,73 +28,89 @@ const styles = StyleSheet.create({ number: { marginRight: 10, }, -}) +}); class OrderList extends Component { - renderItem(order) { return ( - this.props.onItemClick(order) }> + this.props.onItemClick(order)}> - - + + - - - { order.notes ? : null } + + + {order.notes ? ( + + ) : null} - { `${formatPrice(order.itemsTotal)}` } - { moment.parseZone(order.pickupExpectedAt).format('LT') } + {`${formatPrice(order.itemsTotal)}`} + {moment.parseZone(order.pickupExpectedAt).format('LT')} - ) + ); } render() { - const allOrders = [ ...this.props.newOrders, ...this.props.acceptedOrders, ...this.props.pickedOrders, ...this.props.cancelledOrders, ...this.props.fulfilledOrders, - ] + ]; return ( item['@id'] } + data={allOrders} + keyExtractor={(item, index) => item['@id']} sections={[ { - title: this.props.t('RESTAURANT_ORDER_LIST_NEW_ORDERS', { count: this.props.newOrders.length }), + title: this.props.t('RESTAURANT_ORDER_LIST_NEW_ORDERS', { + count: this.props.newOrders.length, + }), data: this.props.newOrders, }, { - title: this.props.t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', { count: this.props.acceptedOrders.length }), + title: this.props.t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', { + count: this.props.acceptedOrders.length, + }), data: this.props.acceptedOrders, }, { - title: this.props.t('RESTAURANT_ORDER_LIST_PICKED_ORDERS', { count: this.props.pickedOrders.length }), + title: this.props.t('RESTAURANT_ORDER_LIST_PICKED_ORDERS', { + count: this.props.pickedOrders.length, + }), data: this.props.pickedOrders, }, { - title: this.props.t('RESTAURANT_ORDER_LIST_CANCELLED_ORDERS', { count: this.props.cancelledOrders.length }), + title: this.props.t('RESTAURANT_ORDER_LIST_CANCELLED_ORDERS', { + count: this.props.cancelledOrders.length, + }), data: this.props.cancelledOrders, }, { - title: this.props.t('RESTAURANT_ORDER_LIST_FULFILLED_ORDERS', { count: this.props.fulfilledOrders.length }), + title: this.props.t('RESTAURANT_ORDER_LIST_FULFILLED_ORDERS', { + count: this.props.fulfilledOrders.length, + }), data: this.props.fulfilledOrders, }, ]} - renderSectionHeader={ ({ section: { title } } ) => ( - + renderSectionHeader={({ section: { title } }) => ( + {title} )} - renderItem={ ({ item }) => this.renderItem(item) } - ItemSeparatorComponent={ ItemSeparatorComponent } /> - ) + renderItem={({ item }) => this.renderItem(item)} + ItemSeparatorComponent={ItemSeparatorComponent} + /> + ); } } -export default withTranslation()(OrderList) +export default withTranslation()(OrderList); diff --git a/src/navigation/restaurant/components/SwipeToAcceptOrRefuse.js b/src/navigation/restaurant/components/SwipeToAcceptOrRefuse.js index 0fa3c5a49..79d0fde1e 100644 --- a/src/navigation/restaurant/components/SwipeToAcceptOrRefuse.js +++ b/src/navigation/restaurant/components/SwipeToAcceptOrRefuse.js @@ -1,9 +1,9 @@ -import React, { useState } from 'react' -import { StyleSheet, View } from 'react-native' -import { useTranslation } from 'react-i18next' -import { Icon, Text, useColorMode } from 'native-base' -import { SwipeRow } from 'react-native-swipe-list-view' -import FontAwesome from 'react-native-vector-icons/FontAwesome' +import React, { useState } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import { Icon, Text, useColorMode } from 'native-base'; +import { SwipeRow } from 'react-native-swipe-list-view'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; const styles = StyleSheet.create({ swipeBg: { @@ -23,52 +23,63 @@ const styles = StyleSheet.create({ }, }); -const swipeRow = React.createRef() +const swipeRow = React.createRef(); const Comp = ({ onAccept, onRefuse }) => { - - const { colorMode } = useColorMode() - const { t } = useTranslation() + const { colorMode } = useColorMode(); + const { t } = useTranslation(); const onSwipeValueChange = ({ key, value }) => { // TODO Animate color - } + }; - const onRowOpen = (value) => { + const onRowOpen = value => { if (value > 0) { - onAccept() + onAccept(); } else { - onRefuse() + onRefuse(); } - setTimeout(() => swipeRow.current?.closeRow(), 250) - } + setTimeout(() => swipeRow.current?.closeRow(), 250); + }; - const [ openValue, setOpenValue ] = useState(0) + const [openValue, setOpenValue] = useState(0); return ( setOpenValue(event.nativeEvent.layout.width * 0.7) }> + onLayout={event => setOpenValue(event.nativeEvent.layout.width * 0.7)}> - - { t('RESTAURANT_ORDER_BUTTON_ACCEPT') } - { t('RESTAURANT_ORDER_BUTTON_REFUSE') } + leftOpenValue={openValue} + rightOpenValue={openValue * -1} + onRowOpen={onRowOpen} + onSwipeValueChange={onSwipeValueChange} + ref={swipeRow}> + + {t('RESTAURANT_ORDER_BUTTON_ACCEPT')} + {t('RESTAURANT_ORDER_BUTTON_REFUSE')} - + - { t('SWIPE_TO_ACCEPT_REFUSE') } + + {t('SWIPE_TO_ACCEPT_REFUSE')} + - ) -} + ); +}; -export default Comp +export default Comp; diff --git a/src/navigation/restaurant/components/WebSocketIndicator.js b/src/navigation/restaurant/components/WebSocketIndicator.js index a3e36e936..388d12df8 100644 --- a/src/navigation/restaurant/components/WebSocketIndicator.js +++ b/src/navigation/restaurant/components/WebSocketIndicator.js @@ -1,14 +1,20 @@ -import React from 'react' -import { ActivityIndicator, StyleSheet, View } from 'react-native' -import { Text } from 'native-base' -import { withTranslation } from 'react-i18next' +import React from 'react'; +import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import { Text } from 'native-base'; +import { withTranslation } from 'react-i18next'; const WebSocketIndicator = ({ connected, t }) => ( - - { connected ? t('WAITING_FOR_ORDER') : t('CONN_LOST') } - + + + {connected ? t('WAITING_FOR_ORDER') : t('CONN_LOST')} + + -) +); const styles = StyleSheet.create({ container: { @@ -33,6 +39,6 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: '700', }, -}) +}); -export default withTranslation()(WebSocketIndicator) +export default withTranslation()(WebSocketIndicator); From ae9114fedb12f2cd332313c5aa229c9c0bafbd36 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:28:53 -0700 Subject: [PATCH 02/69] re-format: RestaurantNavigator --- .../navigators/RestaurantNavigator.js | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/navigation/navigators/RestaurantNavigator.js b/src/navigation/navigators/RestaurantNavigator.js index c9ba32392..796a80706 100644 --- a/src/navigation/navigators/RestaurantNavigator.js +++ b/src/navigation/navigators/RestaurantNavigator.js @@ -1,91 +1,90 @@ -import React from 'react' -import { createStackNavigator } from '@react-navigation/stack' +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; -import i18n from '../../i18n' -import screens, { headerLeft } from '..' -import { stackNavigatorScreenOptions } from '../styles' -import HeaderRight from '../restaurant/components/HeaderRight' -import SettingsNavigator from '../restaurant/SettingsNavigator' -import OrderNumber from '../../components/OrderNumber' +import i18n from '../../i18n'; +import screens, { headerLeft } from '..'; +import { stackNavigatorScreenOptions } from '../styles'; +import HeaderRight from '../restaurant/components/HeaderRight'; +import SettingsNavigator from '../restaurant/SettingsNavigator'; +import OrderNumber from '../../components/OrderNumber'; -const MainStack = createStackNavigator() +const MainStack = createStackNavigator(); const MainNavigator = () => ( - + { - const restaurant = route.params?.restaurant || { name: '' } + component={screens.RestaurantDashboard} + options={({ navigation, route }) => { + const restaurant = route.params?.restaurant || { name: '' }; return { title: restaurant.name, - headerRight: () => , + headerRight: () => , headerLeft: headerLeft(navigation), - } + }; }} /> ({ - headerTitle: () => , + component={screens.RestaurantOrder} + options={({ route }) => ({ + headerTitle: () => , })} /> -) +); -const RootStack = createStackNavigator() +const RootStack = createStackNavigator(); export default () => ( ( /> -) +); From 10fda650e7557b8a953e8cdb0102852356501514 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:29:11 -0700 Subject: [PATCH 03/69] re-format: NotificationHandler --- src/components/NotificationHandler.js | 331 +++++++++++++++----------- 1 file changed, 186 insertions(+), 145 deletions(-) diff --git a/src/components/NotificationHandler.js b/src/components/NotificationHandler.js index 8258cc930..e66a20148 100644 --- a/src/components/NotificationHandler.js +++ b/src/components/NotificationHandler.js @@ -1,29 +1,37 @@ -import React, { Component } from 'react' -import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native' -import { Icon, Text } from 'native-base' -import { connect } from 'react-redux' -import { withTranslation } from 'react-i18next' -import Sound from 'react-native-sound' -import moment from 'moment' -import Modal from 'react-native-modal' -import { CommonActions } from '@react-navigation/native' -import FontAwesome from 'react-native-vector-icons/FontAwesome' -import Ionicons from 'react-native-vector-icons/Ionicons' - -import PushNotification from '../notifications' -import NavigationHolder from '../NavigationHolder' - -import { clearNotifications, pushNotification, registerPushNotificationToken } from '../redux/App/actions' -import { loadTasks, selectTasksChangedAlertSound } from '../redux/Courier' -import { loadOrder, loadOrderAndNavigate, loadOrderAndPushNotification } from '../redux/Restaurant/actions' -import { message as wsMessage } from '../redux/middlewares/CentrifugoMiddleware/actions' -import tracker from '../analytics/Tracker' -import analyticsEvent from '../analytics/Event' - -import ModalContent from './ModalContent' +import React, { Component } from 'react'; +import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { Icon, Text } from 'native-base'; +import { connect } from 'react-redux'; +import { withTranslation } from 'react-i18next'; +import Sound from 'react-native-sound'; +import moment from 'moment'; +import Modal from 'react-native-modal'; +import { CommonActions } from '@react-navigation/native'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import Ionicons from 'react-native-vector-icons/Ionicons'; + +import PushNotification from '../notifications'; +import NavigationHolder from '../NavigationHolder'; + +import { + clearNotifications, + pushNotification, + registerPushNotificationToken, +} from '../redux/App/actions'; +import { loadTasks, selectTasksChangedAlertSound } from '../redux/Courier'; +import { + loadOrder, + loadOrderAndNavigate, + loadOrderAndPushNotification, +} from '../redux/Restaurant/actions'; +import { message as wsMessage } from '../redux/middlewares/CentrifugoMiddleware/actions'; +import tracker from '../analytics/Tracker'; +import analyticsEvent from '../analytics/Event'; + +import ModalContent from './ModalContent'; // Make sure sound will play even when device is in silent mode -Sound.setCategory('Playback') +Sound.setCategory('Playback'); /** * This component is used @@ -31,262 +39,291 @@ Sound.setCategory('Playback') * 2/ To show notifications when the app is in foreground. */ class NotificationHandler extends Component { - constructor(props) { - super(props) + super(props); this.state = { sound: null, isSoundReady: false, - } + }; } _onTasksChanged(date) { if (this.props.currentRoute !== 'CourierTaskList') { - NavigationHolder.navigate('CourierTaskList', {}) + NavigationHolder.navigate('CourierTaskList', {}); } - this.props.loadTasks(moment(date)) + this.props.loadTasks(moment(date)); } _loadSound() { - const bell = new Sound('misstickle__indian_bell_chime.wav', Sound.MAIN_BUNDLE, (error) => { - - if (error) { - return - } + const bell = new Sound( + 'misstickle__indian_bell_chime.wav', + Sound.MAIN_BUNDLE, + error => { + if (error) { + return; + } - bell.setNumberOfLoops(-1) + bell.setNumberOfLoops(-1); - this.setState({ - sound: bell, - isSoundReady: true, - }) - }) + this.setState({ + sound: bell, + isSoundReady: true, + }); + }, + ); } _startSound() { - const { sound, isSoundReady } = this.state + const { sound, isSoundReady } = this.state; if (isSoundReady) { - sound.play((success) => { + sound.play(success => { if (!success) { - sound.reset() + sound.reset(); } - }) + }); // Clear notifications after 10 seconds - setTimeout(() => this.props.clearNotifications(), 10000) + setTimeout(() => this.props.clearNotifications(), 10000); } } _stopSound() { - const { sound, isSoundReady } = this.state + const { sound, isSoundReady } = this.state; if (isSoundReady) { - sound.stop(() => {}) + sound.stop(() => {}); } } includesNotification(notifications, predicate) { - return notifications.findIndex(predicate) !== -1 + return notifications.findIndex(predicate) !== -1; } componentDidUpdate(prevProps) { if (this.props.notifications !== prevProps.notifications) { if (this.props.notifications.length > 0) { - if (this.includesNotification(this.props.notifications, n => n.event === 'order:created')) { - this._startSound() - } else if (this.includesNotification(this.props.notifications,n => n.event === 'tasks:changed')) { + if ( + this.includesNotification( + this.props.notifications, + n => n.event === 'order:created', + ) + ) { + this._startSound(); + } else if ( + this.includesNotification( + this.props.notifications, + n => n.event === 'tasks:changed', + ) + ) { if (this.props.tasksChangedAlertSound) { - this._startSound() + this._startSound(); } } } else { - this._stopSound() + this._stopSound(); } } } componentDidMount() { - - this._loadSound() + this._loadSound(); PushNotification.configure({ onRegister: token => this.props.registerPushNotificationToken(token), onNotification: message => { - const { event } = message.data + const { event } = message.data; if (event && event.name === 'order:created') { tracker.logEvent( analyticsEvent.restaurant._category, analyticsEvent.restaurant.orderCreatedMessage, - message.foreground ? 'in_app' : 'notification_center') + message.foreground ? 'in_app' : 'notification_center', + ); - const { order } = event.data + const { order } = event.data; // Here in any case, we navigate to the order that was tapped, // it should have been loaded via WebSocket already. - this.props.loadOrderAndNavigate(order) + this.props.loadOrderAndNavigate(order); } if (event && event.name === 'tasks:changed') { tracker.logEvent( analyticsEvent.courier._category, analyticsEvent.courier.tasksChangedMessage, - message.foreground ? 'in_app' : 'notification_center') + message.foreground ? 'in_app' : 'notification_center', + ); if (message.foreground) { - this.props.pushNotification('tasks:changed', { date: event.data.date }) + this.props.pushNotification('tasks:changed', { + date: event.data.date, + }); } else { // user clicked on a notification in the notification center - this._onTasksChanged(event.data.date) + this._onTasksChanged(event.data.date); } } }, onBackgroundMessage: message => { - const { event } = message.data + const { event } = message.data; if (event && event.name === 'order:created') { - this.props.loadOrder(event.data.order, (order) => { + this.props.loadOrder(event.data.order, order => { if (order) { // Simulate a WebSocket message this.props.message({ name: 'order:created', data: { order }, - }) + }); } - }) + }); } }, - }) + }); } componentWillUnmount() { - PushNotification.removeListeners() + PushNotification.removeListeners(); } _keyExtractor(item, index) { - switch (item.event) { case 'order:created': - return `order:created:${item.params.order.id}` + return `order:created:${item.params.order.id}`; case 'tasks:changed': - return `tasks:changed:${moment()}` + return `tasks:changed:${moment()}`; } } renderItem(notification) { switch (notification.event) { case 'order:created': - return this.renderOrderCreated(notification.params.order) + return this.renderOrderCreated(notification.params.order); case 'tasks:changed': - return this.renderTasksChanged(notification.params.date, notification.params.added, notification.params.removed) + return this.renderTasksChanged( + notification.params.date, + notification.params.added, + notification.params.removed, + ); } } _navigateToOrder(order) { + this._stopSound(); + this.props.clearNotifications(); - this._stopSound() - this.props.clearNotifications() - - NavigationHolder.dispatch(CommonActions.navigate({ - name: 'RestaurantNav', - params: { - screen: 'Main', + NavigationHolder.dispatch( + CommonActions.navigate({ + name: 'RestaurantNav', params: { - restaurant: order.restaurant, - // We don't want to load orders again when navigating - loadOrders: false, - screen: 'RestaurantOrder', + screen: 'Main', params: { - order, + restaurant: order.restaurant, + // We don't want to load orders again when navigating + loadOrders: false, + screen: 'RestaurantOrder', + params: { + order, + }, }, }, - }, - })) + }), + ); } _navigateToTasks(date) { + this._stopSound(); + this.props.clearNotifications(); - this._stopSound() - this.props.clearNotifications() - - NavigationHolder.dispatch(CommonActions.navigate({ - name: 'CourierNav', - params: { - screen: 'CourierHome', + NavigationHolder.dispatch( + CommonActions.navigate({ + name: 'CourierNav', params: { - screen: 'CourierTaskList', + screen: 'CourierHome', + params: { + screen: 'CourierTaskList', + }, }, - }, - })) + }), + ); - this.props.loadTasks(moment(date)) + this.props.loadTasks(moment(date)); } renderOrderCreated(order) { - return ( - this._navigateToOrder(order) }> - - { this.props.t('NOTIFICATION_ORDER_CREATED_TITLE') } - + this._navigateToOrder(order)}> + {this.props.t('NOTIFICATION_ORDER_CREATED_TITLE')} - ) + ); } renderTasksChanged(date, added, removed) { - return ( - this._navigateToTasks(date) }> + this._navigateToTasks(date)}> - { this.props.t('NOTIFICATION_TASKS_CHANGED_TITLE') } + {this.props.t('NOTIFICATION_TASKS_CHANGED_TITLE')} - { (added && Array.isArray(added) && added.length > 0) && ( - - { this.props.t('NOTIFICATION_TASKS_ADDED', { count: added.length }) } - - ) } - { (removed && Array.isArray(removed) && removed.length > 0) && ( - - { this.props.t('NOTIFICATION_TASKS_REMOVED', { count: removed.length }) } - - ) } + {added && Array.isArray(added) && added.length > 0 && ( + + {this.props.t('NOTIFICATION_TASKS_ADDED', { + count: added.length, + })} + + )} + {removed && Array.isArray(removed) && removed.length > 0 && ( + + {this.props.t('NOTIFICATION_TASKS_REMOVED', { + count: removed.length, + })} + + )} - ) + ); } renderModalContent() { - return ( - - - { this.props.t('NEW_NOTIFICATION') } + + + + {this.props.t('NEW_NOTIFICATION')} + this.renderItem(item) } /> - this.props.clearNotifications() }> - - { this.props.t('CLOSE') } - + data={this.props.notifications} + keyExtractor={this._keyExtractor} + renderItem={({ item }) => this.renderItem(item)} + /> + this.props.clearNotifications()}> + {this.props.t('CLOSE')} - ) + ); } render() { - return ( - - { this.renderModalContent() } + + {this.renderModalContent()} - ) + ); } } @@ -313,30 +350,34 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'space-between', }, -}) +}); function mapStateToProps(state) { - return { currentRoute: state.app.currentRoute, isModalVisible: state.app.notifications.length > 0, notifications: state.app.notifications, tasksChangedAlertSound: selectTasksChangedAlertSound(state), - } + }; } -function mapDispatchToProps (dispatch) { - +function mapDispatchToProps(dispatch) { return { loadOrder: (order, cb) => dispatch(loadOrder(order, cb)), loadOrderAndNavigate: order => dispatch(loadOrderAndNavigate(order)), - loadOrderAndPushNotification: order => dispatch(loadOrderAndPushNotification(order)), - loadTasks: (date) => dispatch(loadTasks(date)), - registerPushNotificationToken: token => dispatch(registerPushNotificationToken(token)), + loadOrderAndPushNotification: order => + dispatch(loadOrderAndPushNotification(order)), + loadTasks: date => dispatch(loadTasks(date)), + registerPushNotificationToken: token => + dispatch(registerPushNotificationToken(token)), clearNotifications: () => dispatch(clearNotifications()), - pushNotification: (event, params) => dispatch(pushNotification(event, params)), + pushNotification: (event, params) => + dispatch(pushNotification(event, params)), message: payload => dispatch(wsMessage(payload)), - } + }; } -export default connect(mapStateToProps, mapDispatchToProps)(withTranslation()(NotificationHandler)) +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withTranslation()(NotificationHandler)); From af929da8a7f7f463494efab99605cca40a8dd39d Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:49:39 -0700 Subject: [PATCH 04/69] refactored dashboard using function components --- src/navigation/restaurant/Dashboard.js | 309 +++++++----------- .../restaurant/components/OrderList.js | 131 ++++---- 2 files changed, 187 insertions(+), 253 deletions(-) diff --git a/src/navigation/restaurant/Dashboard.js b/src/navigation/restaurant/Dashboard.js index 439e24f8f..085407f64 100644 --- a/src/navigation/restaurant/Dashboard.js +++ b/src/navigation/restaurant/Dashboard.js @@ -1,8 +1,8 @@ -import React, { Component } from 'react'; -import { Alert, InteractionManager, NativeModules, View } from 'react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Alert, NativeModules } from 'react-native'; import { Center, VStack } from 'native-base'; -import { connect } from 'react-redux'; -import { withTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake'; import moment from 'moment'; @@ -21,14 +21,7 @@ import { loadOrders, } from '../../redux/Restaurant/actions'; import { connect as connectCentrifugo } from '../../redux/middlewares/CentrifugoMiddleware/actions'; -import { - selectAcceptedOrders, - selectCancelledOrders, - selectFulfilledOrders, - selectNewOrders, - selectPickedOrders, - selectSpecialOpeningHoursSpecificationForToday, -} from '../../redux/Restaurant/selectors'; +import { selectSpecialOpeningHoursSpecificationForToday } from '../../redux/Restaurant/selectors'; import { selectIsCentrifugoConnected, selectIsLoading, @@ -37,26 +30,93 @@ import PushNotification from '../../notifications'; const RNSound = NativeModules.RNSound; -class DashboardPage extends Component { - constructor(props) { - super(props); +export default function DashboardPage({ navigation, route }) { + const restaurant = useSelector(state => state.restaurant.restaurant); + const date = useSelector(state => state.restaurant.date); + const specialOpeningHoursSpecification = useSelector( + selectSpecialOpeningHoursSpecificationForToday, + ); + + const isInternetReachable = useSelector( + state => state.app.isInternetReachable, + ); + const isLoading = useSelector(selectIsLoading); + const isCentrifugoConnected = useSelector(selectIsCentrifugoConnected); + + const { navigate } = navigation; + + const [wasAlertShown, setWasAlertShown] = useState(false); + + const { t } = useTranslation(); + + const dispatch = useDispatch(); + + useEffect(() => { + activateKeepAwakeAsync(); - this.state = { - wasAlertShown: false, + return () => { + deactivateKeepAwake(); }; - } + }, []); + + useEffect(() => { + if (!isCentrifugoConnected) { + dispatch(connectCentrifugo()); + } + }, [dispatch, isCentrifugoConnected]); + + useEffect(() => { + if (route.params?.loadOrders ?? true) { + dispatch( + loadOrders(restaurant, date.format('YYYY-MM-DD'), () => { + // If getInitialNotification returns something, + // it means the app was opened from a quit state. + // + // We handle this here, and *NOT* in NotificationHandler, + // because when the app opens from a quit state, + // NotificationHandler.componentDidMount is called too early. + // + // It tries to call loadOrderAndNavigate, and it fails + // because Redux is not completely ready. + // + // It's not a big issue to handle this here, + // because as the app was opened from a quit state, + // the home screen will be this one (for restaurants). + // + // @see https://rnfirebase.io/messaging/notifications#handling-interaction + PushNotification.getInitialNotification().then(remoteMessage => { + if (remoteMessage) { + const { event } = remoteMessage.data; + if (event && event.name === 'order:created') { + dispatch(loadOrderAndNavigate(event.data.order)); + } + } + }); + }), + ); + } + }, [restaurant, date, dispatch, route.params?.loadOrders]); - _checkSystemVolume() { + // This is needed to display the title + useEffect(() => { + // WARNING Make sure to call navigation.setParams() only when needed to avoid infinite loop + const navRestaurant = route.params?.restaurant; + if (!navRestaurant || navRestaurant !== restaurant) { + navigation.setParams({ restaurant: restaurant }); + } + }, [restaurant, navigation, route.params?.restaurant]); + + const _checkSystemVolume = useCallback(() => { RNSound.getSystemVolume(volume => { if (volume < 0.4) { Alert.alert( - this.props.t('RESTAURANT_SOUND_ALERT_TITLE'), - this.props.t('RESTAURANT_SOUND_ALERT_MESSAGE'), + t('RESTAURANT_SOUND_ALERT_TITLE'), + t('RESTAURANT_SOUND_ALERT_MESSAGE'), [ { - text: this.props.t('RESTAURANT_SOUND_ALERT_CONFIRM'), + text: t('RESTAURANT_SOUND_ALERT_CONFIRM'), onPress: () => { - this.setState({ wasAlertShown: true }); + setWasAlertShown(true); // If would be cool to open the device settings directly, // but it is not (yet) possible to sent an Intent with extra flags @@ -68,188 +128,61 @@ class DashboardPage extends Component { }, }, { - text: this.props.t('CANCEL'), + text: t('CANCEL'), style: 'cancel', - onPress: () => this.setState({ wasAlertShown: true }), + onPress: () => setWasAlertShown(true), }, ], ); } }); - } - - componentDidMount() { - activateKeepAwakeAsync(); - - if (!this.props.isCentrifugoConnected) { - this.props.connectCent(); - } - - InteractionManager.runAfterInteractions(() => { - if (this.props.route.params?.loadOrders || true) { - this.props.loadOrders( - this.props.restaurant, - this.props.date.format('YYYY-MM-DD'), - () => { - // If getInitialNotification returns something, - // it means the app was opened from a quit state. - // - // We handle this here, and *NOT* in NotificationHandler, - // because when the app opens from a quit state, - // NotificationHandler.componentDidMount is called too early. - // - // It tries to call loadOrderAndNavigate, and it fails - // because Redux is not completely ready. - // - // It's not a big issue to handle this here, - // because as the app was opened from a quit state, - // the home screen will be this one (for restaurants). - // - // @see https://rnfirebase.io/messaging/notifications#handling-interaction - PushNotification.getInitialNotification().then(remoteMessage => { - if (remoteMessage) { - const { event } = remoteMessage.data; - if (event && event.name === 'order:created') { - this.props.loadOrderAndNavigate(event.data.order); - } - } - }); - }, - ); - } - // setTimeout(() => this._checkSystemVolume(), 1500) - }); - } - - componentWillUnmount() { - deactivateKeepAwake(); - } - - componentDidUpdate(prevProps) { - const hasRestaurantChanged = - this.props.restaurant !== prevProps.restaurant && - this.props.restaurant['@id'] !== prevProps.restaurant['@id']; - - const hasChanged = - this.props.date !== prevProps.date || hasRestaurantChanged; - - if (hasChanged) { - this.props.loadOrders( - this.props.restaurant, - this.props.date.format('YYYY-MM-DD'), - ); - } - - // This is needed to display the title - // WARNING Make sure to call navigation.setParams() only when needed to avoid infinite loop - const navRestaurant = this.props.route.params?.restaurant; - if (!navRestaurant || navRestaurant !== this.props.restaurant) { - this.props.navigation.setParams({ restaurant: this.props.restaurant }); - } + }, [t]); + useEffect(() => { // Make sure to show Alert once loading has finished, // or it will be closed on iOS // https://github.com/facebook/react-native/issues/10471 - if ( - !this.state.wasAlertShown && - !this.props.isLoading && - prevProps.isLoading - ) { - this._checkSystemVolume(); - } - } - - renderDashboard() { - const { navigate } = this.props.navigation; - const { date, restaurant, specialOpeningHoursSpecification } = this.props; - - return ( - - {restaurant.state === 'rush' && ( - - this.props.changeStatus(this.props.restaurant, 'normal') - } - /> - )} - {specialOpeningHoursSpecification && ( - - this.props.deleteOpeningHoursSpecification( - specialOpeningHoursSpecification, - ) - } - /> - )} - - navigate('RestaurantDate')} - onTodayClick={() => this.props.changeDate(moment())} - /> - navigate('RestaurantOrder', { order })} - /> - - ); - } - - render() { - if (this.props.isInternetReachable) { - return this.renderDashboard(); + if (!wasAlertShown && !isLoading) { + _checkSystemVolume(); + // setTimeout(() => _checkSystemVolume(), 1500) } + }, [isLoading, wasAlertShown, _checkSystemVolume]); + if (!isInternetReachable) { return (
); } -} - -function mapStateToProps(state) { - return { - httpClient: state.app.httpClient, - baseURL: state.app.baseURL, - orders: state.restaurant.orders, - newOrders: selectNewOrders(state), - acceptedOrders: selectAcceptedOrders(state), - pickedOrders: selectPickedOrders(state), - cancelledOrders: selectCancelledOrders(state), - fulfilledOrders: selectFulfilledOrders(state), - date: state.restaurant.date, - restaurant: state.restaurant.restaurant, - specialOpeningHoursSpecification: - selectSpecialOpeningHoursSpecificationForToday(state), - isInternetReachable: state.app.isInternetReachable, - isLoading: selectIsLoading(state), - isCentrifugoConnected: selectIsCentrifugoConnected(state), - }; -} -function mapDispatchToProps(dispatch) { - return { - loadOrders: (restaurant, date, cb) => - dispatch(loadOrders(restaurant, date, cb)), - loadOrderAndNavigate: order => dispatch(loadOrderAndNavigate(order)), - changeDate: date => dispatch(changeDate(date)), - changeStatus: (restaurant, state) => - dispatch(changeStatus(restaurant, state)), - deleteOpeningHoursSpecification: openingHoursSpecification => - dispatch(deleteOpeningHoursSpecification(openingHoursSpecification)), - connectCent: () => dispatch(connectCentrifugo()), - }; + return ( + + {restaurant.state === 'rush' && ( + dispatch(changeStatus(restaurant, 'normal'))} + /> + )} + {specialOpeningHoursSpecification && ( + + dispatch( + deleteOpeningHoursSpecification(specialOpeningHoursSpecification), + ) + } + /> + )} + + navigate('RestaurantDate')} + onTodayClick={() => dispatch(changeDate(moment()))} + /> + navigate('RestaurantOrder', { order })} + /> + + ); } - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(withTranslation()(DashboardPage)); diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index 3683453a9..5ff447b66 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -1,8 +1,7 @@ -import React, { Component } from 'react'; +import React from 'react'; import { SectionList, StyleSheet, TouchableOpacity, View } from 'react-native'; import { HStack, Icon, Text } from 'native-base'; import moment from 'moment'; -import { withTranslation } from 'react-i18next'; import Ionicons from 'react-native-vector-icons/Ionicons'; import FontAwesome from 'react-native-vector-icons/FontAwesome'; @@ -11,6 +10,15 @@ import OrderNumber from '../../../components/OrderNumber'; import ItemSeparatorComponent from '../../../components/ItemSeparator'; import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon'; import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { + selectAcceptedOrders, + selectCancelledOrders, + selectFulfilledOrders, + selectNewOrders, + selectPickedOrders, +} from '../../../redux/Restaurant/selectors'; const styles = StyleSheet.create({ item: { @@ -30,12 +38,18 @@ const styles = StyleSheet.create({ }, }); -class OrderList extends Component { - renderItem(order) { +export default function OrderList({ onItemClick }) { + const newOrders = useSelector(selectNewOrders); + const acceptedOrders = useSelector(selectAcceptedOrders); + const pickedOrders = useSelector(selectPickedOrders); + const cancelledOrders = useSelector(selectCancelledOrders); + const fulfilledOrders = useSelector(selectFulfilledOrders); + + const { t } = useTranslation(); + + const renderItem = order => { return ( - this.props.onItemClick(order)}> + onItemClick(order)}> @@ -54,63 +68,50 @@ class OrderList extends Component { ); - } + }; - render() { - const allOrders = [ - ...this.props.newOrders, - ...this.props.acceptedOrders, - ...this.props.pickedOrders, - ...this.props.cancelledOrders, - ...this.props.fulfilledOrders, - ]; - - return ( - item['@id']} - sections={[ - { - title: this.props.t('RESTAURANT_ORDER_LIST_NEW_ORDERS', { - count: this.props.newOrders.length, - }), - data: this.props.newOrders, - }, - { - title: this.props.t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', { - count: this.props.acceptedOrders.length, - }), - data: this.props.acceptedOrders, - }, - { - title: this.props.t('RESTAURANT_ORDER_LIST_PICKED_ORDERS', { - count: this.props.pickedOrders.length, - }), - data: this.props.pickedOrders, - }, - { - title: this.props.t('RESTAURANT_ORDER_LIST_CANCELLED_ORDERS', { - count: this.props.cancelledOrders.length, - }), - data: this.props.cancelledOrders, - }, - { - title: this.props.t('RESTAURANT_ORDER_LIST_FULFILLED_ORDERS', { - count: this.props.fulfilledOrders.length, - }), - data: this.props.fulfilledOrders, - }, - ]} - renderSectionHeader={({ section: { title } }) => ( - - {title} - - )} - renderItem={({ item }) => this.renderItem(item)} - ItemSeparatorComponent={ItemSeparatorComponent} - /> - ); - } + return ( + item['@id']} + sections={[ + { + title: t('RESTAURANT_ORDER_LIST_NEW_ORDERS', { + count: newOrders.length, + }), + data: newOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', { + count: acceptedOrders.length, + }), + data: acceptedOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_PICKED_ORDERS', { + count: pickedOrders.length, + }), + data: pickedOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_CANCELLED_ORDERS', { + count: cancelledOrders.length, + }), + data: cancelledOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_FULFILLED_ORDERS', { + count: fulfilledOrders.length, + }), + data: fulfilledOrders, + }, + ]} + renderSectionHeader={({ section: { title } }) => ( + + {title} + + )} + renderItem={({ item }) => renderItem(item)} + ItemSeparatorComponent={ItemSeparatorComponent} + /> + ); } - -export default withTranslation()(OrderList); From a23c5754429aeb55d2af83470a32e5cc1c50d73b Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:51:36 -0700 Subject: [PATCH 05/69] added: in preparation and ready sections --- src/i18n/locales/en.json | 8 +- src/navigation/restaurant/Dashboard.js | 10 +- .../restaurant/components/OrderList.js | 82 ++++++---- src/redux/Restaurant/selectors.js | 144 +++++++++++------- 4 files changed, 154 insertions(+), 90 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a74f77364..d6aeda507 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -146,9 +146,11 @@ "TASKS_CHANGED_ALERT_SOUND": "Tasks changed alert sound", "RESTAURANT_LIST_CLICK_BELOW": "Click on a restaurant in the list below", "RESTAURANT_ORDER_LIST_NEW_ORDERS": "New orders ({{count}})", - "RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS": "Accepted orders ({{count}})", - "RESTAURANT_ORDER_LIST_CANCELLED_ORDERS": "Cancelled orders ({{count}})", - "RESTAURANT_ORDER_LIST_FULFILLED_ORDERS": "Fulfilled orders ({{count}})", + "RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS": "Accepted ({{count}})", + "RESTAURANT_ORDER_LIST_STARTED_ORDERS": "In preparation ({{count}})", + "RESTAURANT_ORDER_LIST_READY_ORDERS": "Ready ({{count}})", + "RESTAURANT_ORDER_LIST_CANCELLED_ORDERS": "Cancelled ({{count}})", + "RESTAURANT_ORDER_LIST_FULFILLED_ORDERS": "Delivered ({{count}})", "RESTAURANT_ORDER_BUTTON_ACCEPT": "Accept", "RESTAURANT_ORDER_BUTTON_REFUSE": "Refuse", "RESTAURANT_ORDER_BUTTON_CANCEL": "Cancel", diff --git a/src/navigation/restaurant/Dashboard.js b/src/navigation/restaurant/Dashboard.js index 085407f64..5a236d7cb 100644 --- a/src/navigation/restaurant/Dashboard.js +++ b/src/navigation/restaurant/Dashboard.js @@ -21,7 +21,11 @@ import { loadOrders, } from '../../redux/Restaurant/actions'; import { connect as connectCentrifugo } from '../../redux/middlewares/CentrifugoMiddleware/actions'; -import { selectSpecialOpeningHoursSpecificationForToday } from '../../redux/Restaurant/selectors'; +import { + selectDate, + selectRestaurant, + selectSpecialOpeningHoursSpecificationForToday, +} from '../../redux/Restaurant/selectors'; import { selectIsCentrifugoConnected, selectIsLoading, @@ -31,8 +35,8 @@ import PushNotification from '../../notifications'; const RNSound = NativeModules.RNSound; export default function DashboardPage({ navigation, route }) { - const restaurant = useSelector(state => state.restaurant.restaurant); - const date = useSelector(state => state.restaurant.date); + const restaurant = useSelector(selectRestaurant); + const date = useSelector(selectDate); const specialOpeningHoursSpecification = useSelector( selectSpecialOpeningHoursSpecificationForToday, ); diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index 5ff447b66..ac09615f0 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -18,6 +18,8 @@ import { selectFulfilledOrders, selectNewOrders, selectPickedOrders, + selectReadyOrders, + selectStartedOrders, } from '../../../redux/Restaurant/selectors'; const styles = StyleSheet.create({ @@ -41,12 +43,59 @@ const styles = StyleSheet.create({ export default function OrderList({ onItemClick }) { const newOrders = useSelector(selectNewOrders); const acceptedOrders = useSelector(selectAcceptedOrders); + const startedOrders = useSelector(selectStartedOrders); + const readyOrders = useSelector(selectReadyOrders); const pickedOrders = useSelector(selectPickedOrders); const cancelledOrders = useSelector(selectCancelledOrders); const fulfilledOrders = useSelector(selectFulfilledOrders); const { t } = useTranslation(); + const sections = [ + { + title: t('RESTAURANT_ORDER_LIST_NEW_ORDERS', { + count: newOrders.length, + }), + data: newOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', { + count: acceptedOrders.length, + }), + data: acceptedOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_STARTED_ORDERS', { + count: startedOrders.length, + }), + data: startedOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_READY_ORDERS', { + count: readyOrders.length, + }), + data: readyOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_PICKED_ORDERS', { + count: pickedOrders.length, + }), + data: pickedOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_CANCELLED_ORDERS', { + count: cancelledOrders.length, + }), + data: cancelledOrders, + }, + { + title: t('RESTAURANT_ORDER_LIST_FULFILLED_ORDERS', { + count: fulfilledOrders.length, + }), + data: fulfilledOrders, + }, + ]; + const renderItem = order => { return ( onItemClick(order)}> @@ -73,38 +122,7 @@ export default function OrderList({ onItemClick }) { return ( item['@id']} - sections={[ - { - title: t('RESTAURANT_ORDER_LIST_NEW_ORDERS', { - count: newOrders.length, - }), - data: newOrders, - }, - { - title: t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', { - count: acceptedOrders.length, - }), - data: acceptedOrders, - }, - { - title: t('RESTAURANT_ORDER_LIST_PICKED_ORDERS', { - count: pickedOrders.length, - }), - data: pickedOrders, - }, - { - title: t('RESTAURANT_ORDER_LIST_CANCELLED_ORDERS', { - count: cancelledOrders.length, - }), - data: cancelledOrders, - }, - { - title: t('RESTAURANT_ORDER_LIST_FULFILLED_ORDERS', { - count: fulfilledOrders.length, - }), - data: fulfilledOrders, - }, - ]} + sections={sections} renderSectionHeader={({ section: { title } }) => ( {title} diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index fbd1fd7d9..167cbb6c9 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -1,79 +1,119 @@ -import { createSelector } from 'reselect' -import { find } from 'lodash' -import moment from 'moment' -import _ from 'lodash' -import { matchesDate } from '../../utils/order' +import { createSelector } from 'reselect'; +import { find } from 'lodash'; +import moment from 'moment'; +import _ from 'lodash'; +import { matchesDate } from '../../utils/order'; -const _selectDate = state => state.restaurant.date -const _selectOrders = state => state.restaurant.orders +export const selectRestaurant = state => state.restaurant.restaurant; +export const selectDate = state => state.restaurant.date; + +const _selectOrders = state => state.restaurant.orders; export const selectSpecialOpeningHoursSpecificationForToday = createSelector( - state => state.restaurant.restaurant, - _selectDate, + selectRestaurant, + selectDate, (restaurant, date) => { - if (!Array.isArray(restaurant?.specialOpeningHoursSpecification)) { - - return null + return null; } - return find(restaurant.specialOpeningHoursSpecification, openingHoursSpecification => { - - return date.isSame(moment(openingHoursSpecification.validFrom, 'YYYY-MM-DD'), 'day') - }) - } -) + return find( + restaurant.specialOpeningHoursSpecification, + openingHoursSpecification => { + return date.isSame( + moment(openingHoursSpecification.validFrom, 'YYYY-MM-DD'), + 'day', + ); + }, + ); + }, +); export const selectSpecialOpeningHoursSpecification = createSelector( - state => state.restaurant.restaurant, - (restaurant) => { - + selectRestaurant, + restaurant => { if (restaurant) { - return restaurant.specialOpeningHoursSpecification + return restaurant.specialOpeningHoursSpecification; } - return [] - } -) + return []; + }, +); export const selectNewOrders = createSelector( - _selectDate, + selectDate, _selectOrders, - (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && o.state === 'new'), - [o => moment.parseZone(o.pickupExpectedAt)] - ) -) + (date, orders) => + _.sortBy( + _.filter(orders, o => matchesDate(o, date) && o.state === 'new'), + [o => moment.parseZone(o.pickupExpectedAt)], + ), +); export const selectAcceptedOrders = createSelector( - _selectDate, + selectDate, + _selectOrders, + (date, orders) => + _.sortBy( + _.filter(orders, o => matchesDate(o, date) && o.state === 'accepted'), + [o => moment.parseZone(o.pickupExpectedAt)], + ), +); + +export const selectStartedOrders = createSelector( + selectDate, + _selectOrders, + (date, orders) => + _.sortBy( + _.filter(orders, o => matchesDate(o, date) && o.state === 'started'), + [o => moment.parseZone(o.pickupExpectedAt)], + ), +); + +export const selectReadyOrders = createSelector( + selectDate, _selectOrders, - (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && o.state === 'accepted' && !o.assignedTo), - [o => moment.parseZone(o.pickupExpectedAt)] - ) -) + (date, orders) => + _.sortBy( + _.filter( + orders, + o => matchesDate(o, date) && o.state === 'ready' && !o.assignedTo, + ), + [o => moment.parseZone(o.pickupExpectedAt)], + ), +); export const selectPickedOrders = createSelector( - _selectDate, + selectDate, _selectOrders, - (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && o.state === 'accepted' && !!o.assignedTo), - [o => moment.parseZone(o.pickupExpectedAt)] - ) -) + (date, orders) => + _.sortBy( + _.filter( + orders, + o => matchesDate(o, date) && o.state === 'ready' && !!o.assignedTo, + ), + [o => moment.parseZone(o.pickupExpectedAt)], + ), +); export const selectCancelledOrders = createSelector( - _selectDate, + selectDate, _selectOrders, - (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && (o.state === 'refused' || o.state === 'cancelled')), - [o => moment.parseZone(o.pickupExpectedAt)] - ) -) + (date, orders) => + _.sortBy( + _.filter( + orders, + o => + matchesDate(o, date) && + (o.state === 'refused' || o.state === 'cancelled'), + ), + [o => moment.parseZone(o.pickupExpectedAt)], + ), +); export const selectFulfilledOrders = createSelector( - _selectDate, + selectDate, _selectOrders, - (date, orders) => _.filter(orders, o => matchesDate(o, date) && o.state === 'fulfilled') -) + (date, orders) => + _.filter(orders, o => matchesDate(o, date) && o.state === 'fulfilled'), +); From db486269743e36ff3bf98d471bd9b4a264ee2e11 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:04:48 -0700 Subject: [PATCH 06/69] hide new orders section for the restaurants with autoAcceptOrdersEnabled --- .../restaurant/components/OrderList.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index ac09615f0..6f374e4e9 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -19,6 +19,7 @@ import { selectNewOrders, selectPickedOrders, selectReadyOrders, + selectRestaurant, selectStartedOrders, } from '../../../redux/Restaurant/selectors'; @@ -41,6 +42,8 @@ const styles = StyleSheet.create({ }); export default function OrderList({ onItemClick }) { + const restaurant = useSelector(selectRestaurant); + const newOrders = useSelector(selectNewOrders); const acceptedOrders = useSelector(selectAcceptedOrders); const startedOrders = useSelector(selectStartedOrders); @@ -52,12 +55,16 @@ export default function OrderList({ onItemClick }) { const { t } = useTranslation(); const sections = [ - { - title: t('RESTAURANT_ORDER_LIST_NEW_ORDERS', { - count: newOrders.length, - }), - data: newOrders, - }, + ...(restaurant.autoAcceptOrdersEnabled + ? [] + : [ + { + title: t('RESTAURANT_ORDER_LIST_NEW_ORDERS', { + count: newOrders.length, + }), + data: newOrders, + }, + ]), { title: t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', { count: acceptedOrders.length, From b11960aa20c80653790e6b0b8295cc47fe27390e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:08:59 -0700 Subject: [PATCH 07/69] extracted OrderItem --- .../restaurant/components/OrderItem.js | 47 +++++++++++++++++ .../restaurant/components/OrderList.js | 51 +++---------------- 2 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 src/navigation/restaurant/components/OrderItem.js diff --git a/src/navigation/restaurant/components/OrderItem.js b/src/navigation/restaurant/components/OrderItem.js new file mode 100644 index 000000000..3347344e3 --- /dev/null +++ b/src/navigation/restaurant/components/OrderItem.js @@ -0,0 +1,47 @@ +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { HStack, Icon, Text } from 'native-base'; +import OrderNumber from '../../../components/OrderNumber'; +import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon'; +import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import { formatPrice } from '../../../utils/formatting'; +import moment from 'moment/moment'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import React from 'react'; + +const styles = StyleSheet.create({ + item: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 15, + paddingHorizontal: 20, + }, + number: { + marginRight: 10, + }, +}); + +export default function OrderItem({ order, onItemClick }) { + return ( + onItemClick(order)}> + + + + + + + {order.notes ? ( + + ) : null} + + {`${formatPrice(order.itemsTotal)}`} + {moment.parseZone(order.pickupExpectedAt).format('LT')} + + + ); +} diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index 6f374e4e9..87c13ca74 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -1,15 +1,7 @@ import React from 'react'; -import { SectionList, StyleSheet, TouchableOpacity, View } from 'react-native'; -import { HStack, Icon, Text } from 'native-base'; -import moment from 'moment'; -import Ionicons from 'react-native-vector-icons/Ionicons'; -import FontAwesome from 'react-native-vector-icons/FontAwesome'; - -import { formatPrice } from '../../../utils/formatting'; -import OrderNumber from '../../../components/OrderNumber'; +import { SectionList, StyleSheet, View } from 'react-native'; +import { Text } from 'native-base'; import ItemSeparatorComponent from '../../../components/ItemSeparator'; -import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon'; -import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { @@ -22,23 +14,13 @@ import { selectRestaurant, selectStartedOrders, } from '../../../redux/Restaurant/selectors'; +import OrderItem from './OrderItem'; const styles = StyleSheet.create({ - item: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingVertical: 15, - paddingHorizontal: 20, - }, sectionHeader: { paddingVertical: 10, paddingHorizontal: 20, }, - number: { - marginRight: 10, - }, }); export default function OrderList({ onItemClick }) { @@ -103,29 +85,6 @@ export default function OrderList({ onItemClick }) { }, ]; - const renderItem = order => { - return ( - onItemClick(order)}> - - - - - - - {order.notes ? ( - - ) : null} - - {`${formatPrice(order.itemsTotal)}`} - {moment.parseZone(order.pickupExpectedAt).format('LT')} - - - ); - }; - return ( item['@id']} @@ -135,7 +94,9 @@ export default function OrderList({ onItemClick }) { {title} )} - renderItem={({ item }) => renderItem(item)} + renderItem={({ item }) => ( + + )} ItemSeparatorComponent={ItemSeparatorComponent} /> ); From f042532c93c8b9c06845780f5a2650dc593d2b7c Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 1 Apr 2024 18:15:29 -0700 Subject: [PATCH 08/69] WIP; green button to move order to the next state --- src/model/Order.js | 9 + .../restaurant/components/OrderItem.js | 99 +- .../restaurant/components/OrderList.js | 7 +- src/redux/Restaurant/actions.js | 1103 +++++++++-------- src/redux/Restaurant/selectors.js | 15 +- 5 files changed, 678 insertions(+), 555 deletions(-) create mode 100644 src/model/Order.js diff --git a/src/model/Order.js b/src/model/Order.js new file mode 100644 index 000000000..c3da32370 --- /dev/null +++ b/src/model/Order.js @@ -0,0 +1,9 @@ +export const STATE = { + NEW: 'new', + ACCEPTED: 'accepted', + REFUSED: 'refused', + STARTED: 'started', + READY: 'ready', + FULFILLED: 'fulfilled', + CANCELLED: 'cancelled', +}; diff --git a/src/navigation/restaurant/components/OrderItem.js b/src/navigation/restaurant/components/OrderItem.js index 3347344e3..29cde403c 100644 --- a/src/navigation/restaurant/components/OrderItem.js +++ b/src/navigation/restaurant/components/OrderItem.js @@ -8,40 +8,99 @@ import { formatPrice } from '../../../utils/formatting'; import moment from 'moment/moment'; import Ionicons from 'react-native-vector-icons/Ionicons'; import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + acceptOrder, + startPreparing, + finishPreparing, +} from '../../../redux/Restaurant/actions'; +import { STATE } from '../../../model/Order'; const styles = StyleSheet.create({ item: { + flex: 1, + flexDirection: 'row', + marginVertical: 6, + marginLeft: 24 + 16, + marginRight: 24, + borderColor: '#E3E3E3', + borderWidth: 1, + borderRadius: 4, + }, + content: { + padding: 12, flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingVertical: 15, - paddingHorizontal: 20, }, number: { marginRight: 10, }, + moveForward: { + backgroundColor: '#5EBE68', + paddingHorizontal: 12, + justifyContent: 'center', + }, + moveForwardIcon: { + color: 'white', + }, }); export default function OrderItem({ order, onItemClick }) { + const dispatch = useDispatch(); + + const isActionable = [STATE.NEW, STATE.ACCEPTED, STATE.STARTED].includes( + order.state, + ); + return ( - onItemClick(order)}> - - - - - - - {order.notes ? ( - - ) : null} - - {`${formatPrice(order.itemsTotal)}`} - {moment.parseZone(order.pickupExpectedAt).format('LT')} - - + + onItemClick(order)}> + + + + + + + {order.notes ? ( + + ) : null} + + {`${formatPrice(order.itemsTotal)}`} + {moment.parseZone(order.pickupExpectedAt).format('LT')} + + {isActionable ? ( + { + switch (order.state) { + case STATE.NEW: + dispatch(acceptOrder(order)); + break; + case STATE.ACCEPTED: + dispatch(startPreparing(order)); + break; + case STATE.STARTED: + dispatch(finishPreparing(order)); + break; + default: + break; + } + }}> + + + ) : null} + ); } diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index 87c13ca74..0a0e3a019 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -1,7 +1,6 @@ import React from 'react'; import { SectionList, StyleSheet, View } from 'react-native'; import { Text } from 'native-base'; -import ItemSeparatorComponent from '../../../components/ItemSeparator'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { @@ -18,8 +17,9 @@ import OrderItem from './OrderItem'; const styles = StyleSheet.create({ sectionHeader: { - paddingVertical: 10, - paddingHorizontal: 20, + paddingTop: 32, + paddingBottom: 12, + paddingHorizontal: 24, }, }); @@ -97,7 +97,6 @@ export default function OrderList({ onItemClick }) { renderItem={({ item }) => ( )} - ItemSeparatorComponent={ItemSeparatorComponent} /> ); } diff --git a/src/redux/Restaurant/actions.js b/src/redux/Restaurant/actions.js index 2d9ce1838..18abbde17 100644 --- a/src/redux/Restaurant/actions.js +++ b/src/redux/Restaurant/actions.js @@ -1,768 +1,817 @@ -import { createAction } from 'redux-actions' -import { CommonActions } from '@react-navigation/native' -import BleManager from 'react-native-ble-manager' -import _ from 'lodash' -import { Buffer } from 'buffer' +import { createAction } from 'redux-actions'; +import { CommonActions } from '@react-navigation/native'; +import BleManager from 'react-native-ble-manager'; +import _ from 'lodash'; +import { Buffer } from 'buffer'; -import DropdownHolder from '../../DropdownHolder' -import NavigationHolder from '../../NavigationHolder' +import DropdownHolder from '../../DropdownHolder'; +import NavigationHolder from '../../NavigationHolder'; -import { pushNotification } from '../App/actions' -import { encodeForPrinter } from '../../utils/order' -import * as SunmiPrinterLibrary from '@mitsuharu/react-native-sunmi-printer-library' +import { pushNotification } from '../App/actions'; +import { encodeForPrinter } from '../../utils/order'; +import * as SunmiPrinterLibrary from '@mitsuharu/react-native-sunmi-printer-library'; -import i18n from '../../i18n' +import i18n from '../../i18n'; import { LOAD_MY_RESTAURANTS_FAILURE, LOAD_MY_RESTAURANTS_REQUEST, LOAD_MY_RESTAURANTS_SUCCESS, -} from '../App/actions' +} from '../App/actions'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { selectHttpClient } from '../App/selectors'; /* * Action Types */ -export const LOAD_ORDERS_REQUEST = 'LOAD_ORDERS_REQUEST' -export const LOAD_ORDERS_SUCCESS = 'LOAD_ORDERS_SUCCESS' -export const LOAD_ORDERS_FAILURE = 'LOAD_ORDERS_FAILURE' +export const LOAD_ORDERS_REQUEST = 'LOAD_ORDERS_REQUEST'; +export const LOAD_ORDERS_SUCCESS = 'LOAD_ORDERS_SUCCESS'; +export const LOAD_ORDERS_FAILURE = 'LOAD_ORDERS_FAILURE'; -export const LOAD_ORDER_REQUEST = 'LOAD_ORDER_REQUEST' -export const LOAD_ORDER_SUCCESS = 'LOAD_ORDER_SUCCESS' -export const LOAD_ORDER_FAILURE = 'LOAD_ORDER_FAILURE' +export const LOAD_ORDER_REQUEST = 'LOAD_ORDER_REQUEST'; +export const LOAD_ORDER_SUCCESS = 'LOAD_ORDER_SUCCESS'; +export const LOAD_ORDER_FAILURE = 'LOAD_ORDER_FAILURE'; -export const ACCEPT_ORDER_REQUEST = 'ACCEPT_ORDER_REQUEST' -export const ACCEPT_ORDER_SUCCESS = 'ACCEPT_ORDER_SUCCESS' -export const ACCEPT_ORDER_FAILURE = 'ACCEPT_ORDER_FAILURE' +export const ACCEPT_ORDER_REQUEST = 'ACCEPT_ORDER_REQUEST'; +export const ACCEPT_ORDER_SUCCESS = 'ACCEPT_ORDER_SUCCESS'; +export const ACCEPT_ORDER_FAILURE = 'ACCEPT_ORDER_FAILURE'; -export const REFUSE_ORDER_REQUEST = 'REFUSE_ORDER_REQUEST' -export const REFUSE_ORDER_SUCCESS = 'REFUSE_ORDER_SUCCESS' -export const REFUSE_ORDER_FAILURE = 'REFUSE_ORDER_FAILURE' +export const REFUSE_ORDER_REQUEST = 'REFUSE_ORDER_REQUEST'; +export const REFUSE_ORDER_SUCCESS = 'REFUSE_ORDER_SUCCESS'; +export const REFUSE_ORDER_FAILURE = 'REFUSE_ORDER_FAILURE'; -export const DELAY_ORDER_REQUEST = 'DELAY_ORDER_REQUEST' -export const DELAY_ORDER_SUCCESS = 'DELAY_ORDER_SUCCESS' -export const DELAY_ORDER_FAILURE = 'DELAY_ORDER_FAILURE' +export const DELAY_ORDER_REQUEST = 'DELAY_ORDER_REQUEST'; +export const DELAY_ORDER_SUCCESS = 'DELAY_ORDER_SUCCESS'; +export const DELAY_ORDER_FAILURE = 'DELAY_ORDER_FAILURE'; -export const FULFILL_ORDER_REQUEST = 'FULFILL_ORDER_REQUEST' -export const FULFILL_ORDER_SUCCESS = 'FULFILL_ORDER_SUCCESS' -export const FULFILL_ORDER_FAILURE = 'FULFILL_ORDER_FAILURE' +export const FULFILL_ORDER_REQUEST = 'FULFILL_ORDER_REQUEST'; +export const FULFILL_ORDER_SUCCESS = 'FULFILL_ORDER_SUCCESS'; +export const FULFILL_ORDER_FAILURE = 'FULFILL_ORDER_FAILURE'; -export const CANCEL_ORDER_REQUEST = 'CANCEL_ORDER_REQUEST' -export const CANCEL_ORDER_SUCCESS = 'CANCEL_ORDER_SUCCESS' -export const CANCEL_ORDER_FAILURE = 'CANCEL_ORDER_FAILURE' +export const CANCEL_ORDER_REQUEST = 'CANCEL_ORDER_REQUEST'; +export const CANCEL_ORDER_SUCCESS = 'CANCEL_ORDER_SUCCESS'; +export const CANCEL_ORDER_FAILURE = 'CANCEL_ORDER_FAILURE'; -export const CHANGE_STATUS_REQUEST = 'CHANGE_STATUS_REQUEST' -export const CHANGE_STATUS_SUCCESS = 'CHANGE_STATUS_SUCCESS' -export const CHANGE_STATUS_FAILURE = 'CHANGE_STATUS_FAILURE' +export const CHANGE_STATUS_REQUEST = 'CHANGE_STATUS_REQUEST'; +export const CHANGE_STATUS_SUCCESS = 'CHANGE_STATUS_SUCCESS'; +export const CHANGE_STATUS_FAILURE = 'CHANGE_STATUS_FAILURE'; -export const CHANGE_RESTAURANT = 'CHANGE_RESTAURANT' -export const CHANGE_DATE = 'CHANGE_DATE' +export const CHANGE_RESTAURANT = 'CHANGE_RESTAURANT'; +export const CHANGE_DATE = 'CHANGE_DATE'; -export const LOAD_PRODUCTS_REQUEST = 'LOAD_PRODUCTS_REQUEST' -export const LOAD_PRODUCTS_SUCCESS = 'LOAD_PRODUCTS_SUCCESS' -export const LOAD_PRODUCTS_FAILURE = 'LOAD_PRODUCTS_FAILURE' +export const LOAD_PRODUCTS_REQUEST = 'LOAD_PRODUCTS_REQUEST'; +export const LOAD_PRODUCTS_SUCCESS = 'LOAD_PRODUCTS_SUCCESS'; +export const LOAD_PRODUCTS_FAILURE = 'LOAD_PRODUCTS_FAILURE'; -export const LOAD_PRODUCT_OPTIONS_SUCCESS = 'LOAD_PRODUCT_OPTIONS_SUCCESS' +export const LOAD_PRODUCT_OPTIONS_SUCCESS = 'LOAD_PRODUCT_OPTIONS_SUCCESS'; -export const LOAD_MENUS_REQUEST = 'LOAD_MENUS_REQUEST' -export const LOAD_MENUS_SUCCESS = 'LOAD_MENUS_SUCCESS' -export const LOAD_MENUS_FAILURE = 'LOAD_MENUS_FAILURE' -export const SET_CURRENT_MENU = 'SET_CURRENT_MENU' +export const LOAD_MENUS_REQUEST = 'LOAD_MENUS_REQUEST'; +export const LOAD_MENUS_SUCCESS = 'LOAD_MENUS_SUCCESS'; +export const LOAD_MENUS_FAILURE = 'LOAD_MENUS_FAILURE'; +export const SET_CURRENT_MENU = 'SET_CURRENT_MENU'; -export const SET_NEXT_PRODUCTS_PAGE = 'SET_NEXT_PRODUCTS_PAGE' -export const SET_HAS_MORE_PRODUCTS = 'SET_HAS_MORE_PRODUCTS' +export const SET_NEXT_PRODUCTS_PAGE = 'SET_NEXT_PRODUCTS_PAGE'; +export const SET_HAS_MORE_PRODUCTS = 'SET_HAS_MORE_PRODUCTS'; -export const LOAD_MORE_PRODUCTS_SUCCESS = 'LOAD_MORE_PRODUCTS_SUCCESS' +export const LOAD_MORE_PRODUCTS_SUCCESS = 'LOAD_MORE_PRODUCTS_SUCCESS'; -export const CHANGE_PRODUCT_ENABLED_REQUEST = 'CHANGE_PRODUCT_ENABLED_REQUEST' -export const CHANGE_PRODUCT_ENABLED_SUCCESS = 'CHANGE_PRODUCT_ENABLED_SUCCESS' -export const CHANGE_PRODUCT_ENABLED_FAILURE = 'CHANGE_PRODUCT_ENABLED_FAILURE' +export const CHANGE_PRODUCT_ENABLED_REQUEST = 'CHANGE_PRODUCT_ENABLED_REQUEST'; +export const CHANGE_PRODUCT_ENABLED_SUCCESS = 'CHANGE_PRODUCT_ENABLED_SUCCESS'; +export const CHANGE_PRODUCT_ENABLED_FAILURE = 'CHANGE_PRODUCT_ENABLED_FAILURE'; -export const CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST = 'CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST' -export const CHANGE_PRODUCT_OPTION_VALUE_ENABLED_SUCCESS = 'CHANGE_PRODUCT_OPTION_VALUE_ENABLED_SUCCESS' -export const CHANGE_PRODUCT_OPTION_VALUE_ENABLED_FAILURE = 'CHANGE_PRODUCT_OPTION_VALUE_ENABLED_FAILURE' +export const CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST = + 'CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST'; +export const CHANGE_PRODUCT_OPTION_VALUE_ENABLED_SUCCESS = + 'CHANGE_PRODUCT_OPTION_VALUE_ENABLED_SUCCESS'; +export const CHANGE_PRODUCT_OPTION_VALUE_ENABLED_FAILURE = + 'CHANGE_PRODUCT_OPTION_VALUE_ENABLED_FAILURE'; -export const CLOSE_RESTAURANT_REQUEST = 'CLOSE_RESTAURANT_REQUEST' -export const CLOSE_RESTAURANT_SUCCESS = 'CLOSE_RESTAURANT_SUCCESS' -export const CLOSE_RESTAURANT_FAILURE = 'CLOSE_RESTAURANT_FAILURE' +export const CLOSE_RESTAURANT_REQUEST = 'CLOSE_RESTAURANT_REQUEST'; +export const CLOSE_RESTAURANT_SUCCESS = 'CLOSE_RESTAURANT_SUCCESS'; +export const CLOSE_RESTAURANT_FAILURE = 'CLOSE_RESTAURANT_FAILURE'; -export const DELETE_OPENING_HOURS_SPECIFICATION_REQUEST = 'DELETE_OPENING_HOURS_SPECIFICATION_REQUEST' -export const DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS = 'DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS' -export const DELETE_OPENING_HOURS_SPECIFICATION_FAILURE = 'DELETE_OPENING_HOURS_SPECIFICATION_FAILURE' +export const DELETE_OPENING_HOURS_SPECIFICATION_REQUEST = + 'DELETE_OPENING_HOURS_SPECIFICATION_REQUEST'; +export const DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS = + 'DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS'; +export const DELETE_OPENING_HOURS_SPECIFICATION_FAILURE = + 'DELETE_OPENING_HOURS_SPECIFICATION_FAILURE'; -export const PRINTER_CONNECTED = '@restaurant/PRINTER_CONNECTED' -export const PRINTER_DISCONNECTED = '@restaurant/PRINTER_DISCONNECTED' -export const BLUETOOTH_ENABLED = '@restaurant/BLUETOOTH_ENABLED' -export const BLUETOOTH_DISABLED = '@restaurant/BLUETOOTH_DISABLED' -export const BLUETOOTH_START_SCAN = '@restaurant/BLUETOOTH_START_SCAN' -export const BLUETOOTH_STOP_SCAN = '@restaurant/BLUETOOTH_STOP_SCAN' -export const BLUETOOTH_STARTED = '@restaurant/BLUETOOTH_STARTED' +export const PRINTER_CONNECTED = '@restaurant/PRINTER_CONNECTED'; +export const PRINTER_DISCONNECTED = '@restaurant/PRINTER_DISCONNECTED'; +export const BLUETOOTH_ENABLED = '@restaurant/BLUETOOTH_ENABLED'; +export const BLUETOOTH_DISABLED = '@restaurant/BLUETOOTH_DISABLED'; +export const BLUETOOTH_START_SCAN = '@restaurant/BLUETOOTH_START_SCAN'; +export const BLUETOOTH_STOP_SCAN = '@restaurant/BLUETOOTH_STOP_SCAN'; +export const BLUETOOTH_STARTED = '@restaurant/BLUETOOTH_STARTED'; -export const SUNMI_PRINTER_DETECTED = '@restaurant/SUNMI_PRINTER_DETECTED' +export const SUNMI_PRINTER_DETECTED = '@restaurant/SUNMI_PRINTER_DETECTED'; -export const SET_LOOPEAT_FORMATS = '@restaurant/SET_LOOPEAT_FORMATS' -export const UPDATE_LOOPEAT_FORMATS_SUCCESS = '@restaurant/UPDATE_LOOPEAT_FORMATS_SUCCESS' +export const SET_LOOPEAT_FORMATS = '@restaurant/SET_LOOPEAT_FORMATS'; +export const UPDATE_LOOPEAT_FORMATS_SUCCESS = + '@restaurant/UPDATE_LOOPEAT_FORMATS_SUCCESS'; /* * Action Creators */ -const loadMyRestaurantsRequest = createAction(LOAD_MY_RESTAURANTS_REQUEST) -const loadMyRestaurantsSuccess = createAction(LOAD_MY_RESTAURANTS_SUCCESS) -const loadMyRestaurantsFailure = createAction(LOAD_MY_RESTAURANTS_FAILURE) - -export const loadOrdersRequest = createAction(LOAD_ORDERS_REQUEST) -export const loadOrdersSuccess = createAction(LOAD_ORDERS_SUCCESS) -export const loadOrdersFailure = createAction(LOAD_ORDERS_FAILURE) - -export const loadOrderRequest = createAction(LOAD_ORDER_REQUEST) -export const loadOrderSuccess = createAction(LOAD_ORDER_SUCCESS) -export const loadOrderFailure = createAction(LOAD_ORDER_FAILURE) - -export const loadMenusRequest = createAction(LOAD_MENUS_REQUEST) -export const loadMenusSuccess = createAction(LOAD_MENUS_SUCCESS) -export const loadMenusFailure = createAction(LOAD_MENUS_FAILURE) -export const setCurrentMenu = createAction(SET_CURRENT_MENU, (restaurant, menu) => ({ restaurant, menu })) - -export const acceptOrderRequest = createAction(ACCEPT_ORDER_REQUEST) -export const acceptOrderSuccess = createAction(ACCEPT_ORDER_SUCCESS) -export const acceptOrderFailure = createAction(ACCEPT_ORDER_FAILURE) - -export const refuseOrderRequest = createAction(REFUSE_ORDER_REQUEST) -export const refuseOrderSuccess = createAction(REFUSE_ORDER_SUCCESS) -export const refuseOrderFailure = createAction(REFUSE_ORDER_FAILURE) - -export const delayOrderRequest = createAction(DELAY_ORDER_REQUEST) -export const delayOrderSuccess = createAction(DELAY_ORDER_SUCCESS) -export const delayOrderFailure = createAction(DELAY_ORDER_FAILURE) - -export const fulfillOrderRequest = createAction(FULFILL_ORDER_REQUEST) -export const fulfillOrderSuccess = createAction(FULFILL_ORDER_SUCCESS) -export const fulfillOrderFailure = createAction(FULFILL_ORDER_FAILURE) - -export const cancelOrderRequest = createAction(CANCEL_ORDER_REQUEST) -export const cancelOrderSuccess = createAction(CANCEL_ORDER_SUCCESS) -export const cancelOrderFailure = createAction(CANCEL_ORDER_FAILURE) - -export const changeStatusRequest = createAction(CHANGE_STATUS_REQUEST) -export const changeStatusSuccess = createAction(CHANGE_STATUS_SUCCESS) -export const changeStatusFailure = createAction(CHANGE_STATUS_FAILURE) - -export const changeRestaurant = createAction(CHANGE_RESTAURANT) -export const changeDate = createAction(CHANGE_DATE) - -export const loadProductsRequest = createAction(LOAD_PRODUCTS_REQUEST) -export const loadProductsSuccess = createAction(LOAD_PRODUCTS_SUCCESS) -export const loadProductsFailure = createAction(LOAD_PRODUCTS_FAILURE) - -export const loadProductOptionsSuccess = createAction(LOAD_PRODUCT_OPTIONS_SUCCESS) - -export const setNextProductsPage = createAction(SET_NEXT_PRODUCTS_PAGE) -export const loadMoreProductsSuccess = createAction(LOAD_MORE_PRODUCTS_SUCCESS) -export const setHasMoreProducts = createAction(SET_HAS_MORE_PRODUCTS) - -export const changeProductEnabledRequest = createAction(CHANGE_PRODUCT_ENABLED_REQUEST, (product, enabled) => ({ product, enabled })) -export const changeProductEnabledSuccess = createAction(CHANGE_PRODUCT_ENABLED_SUCCESS) -export const changeProductEnabledFailure = createAction(CHANGE_PRODUCT_ENABLED_FAILURE, (error, product, enabled) => ({ error, product, enabled })) - -export const changeProductOptionValueEnabledRequest = createAction(CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST, (productOptionValue, enabled) => ({ productOptionValue, enabled })) -export const changeProductOptionValueEnabledSuccess = createAction(CHANGE_PRODUCT_OPTION_VALUE_ENABLED_SUCCESS, (productOptionValue, enabled) => ({ productOptionValue, enabled })) -export const changeProductOptionValueEnabledFailure = createAction(CHANGE_PRODUCT_OPTION_VALUE_ENABLED_FAILURE, (error, productOptionValue, enabled) => ({ error, productOptionValue, enabled })) - -export const closeRestaurantRequest = createAction(CLOSE_RESTAURANT_REQUEST) -export const closeRestaurantSuccess = createAction(CLOSE_RESTAURANT_SUCCESS) -export const closeRestaurantFailure = createAction(CLOSE_RESTAURANT_FAILURE) - -export const deleteOpeningHoursSpecificationRequest = createAction(DELETE_OPENING_HOURS_SPECIFICATION_REQUEST) -export const deleteOpeningHoursSpecificationSuccess = createAction(DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS) -export const deleteOpeningHoursSpecificationFailure = createAction(DELETE_OPENING_HOURS_SPECIFICATION_FAILURE) - -export const printerConnected = createAction(PRINTER_CONNECTED) -export const printerDisconnected = createAction(PRINTER_DISCONNECTED) - -export const bluetoothEnabled = createAction(BLUETOOTH_ENABLED) -export const bluetoothDisabled = createAction(BLUETOOTH_DISABLED) -const _bluetoothStartScan = createAction(BLUETOOTH_START_SCAN) -export const bluetoothStopScan = createAction(BLUETOOTH_STOP_SCAN) -export const bluetoothStarted = createAction(BLUETOOTH_STARTED) - -export const sunmiPrinterDetected = createAction(SUNMI_PRINTER_DETECTED) - -export const setLoopeatFormats = createAction(SET_LOOPEAT_FORMATS, (order, loopeatFormats) => ({ order, loopeatFormats })) -export const updateLoopeatFormatsSuccess = createAction(UPDATE_LOOPEAT_FORMATS_SUCCESS) - +const loadMyRestaurantsRequest = createAction(LOAD_MY_RESTAURANTS_REQUEST); +const loadMyRestaurantsSuccess = createAction(LOAD_MY_RESTAURANTS_SUCCESS); +const loadMyRestaurantsFailure = createAction(LOAD_MY_RESTAURANTS_FAILURE); + +export const loadOrdersRequest = createAction(LOAD_ORDERS_REQUEST); +export const loadOrdersSuccess = createAction(LOAD_ORDERS_SUCCESS); +export const loadOrdersFailure = createAction(LOAD_ORDERS_FAILURE); + +export const loadOrderRequest = createAction(LOAD_ORDER_REQUEST); +export const loadOrderSuccess = createAction(LOAD_ORDER_SUCCESS); +export const loadOrderFailure = createAction(LOAD_ORDER_FAILURE); + +export const loadMenusRequest = createAction(LOAD_MENUS_REQUEST); +export const loadMenusSuccess = createAction(LOAD_MENUS_SUCCESS); +export const loadMenusFailure = createAction(LOAD_MENUS_FAILURE); +export const setCurrentMenu = createAction( + SET_CURRENT_MENU, + (restaurant, menu) => ({ restaurant, menu }), +); + +export const acceptOrderRequest = createAction(ACCEPT_ORDER_REQUEST); +export const acceptOrderSuccess = createAction(ACCEPT_ORDER_SUCCESS); +export const acceptOrderFailure = createAction(ACCEPT_ORDER_FAILURE); + +export const refuseOrderRequest = createAction(REFUSE_ORDER_REQUEST); +export const refuseOrderSuccess = createAction(REFUSE_ORDER_SUCCESS); +export const refuseOrderFailure = createAction(REFUSE_ORDER_FAILURE); + +export const delayOrderRequest = createAction(DELAY_ORDER_REQUEST); +export const delayOrderSuccess = createAction(DELAY_ORDER_SUCCESS); +export const delayOrderFailure = createAction(DELAY_ORDER_FAILURE); + +export const fulfillOrderRequest = createAction(FULFILL_ORDER_REQUEST); +export const fulfillOrderSuccess = createAction(FULFILL_ORDER_SUCCESS); +export const fulfillOrderFailure = createAction(FULFILL_ORDER_FAILURE); + +export const cancelOrderRequest = createAction(CANCEL_ORDER_REQUEST); +export const cancelOrderSuccess = createAction(CANCEL_ORDER_SUCCESS); +export const cancelOrderFailure = createAction(CANCEL_ORDER_FAILURE); + +export const changeStatusRequest = createAction(CHANGE_STATUS_REQUEST); +export const changeStatusSuccess = createAction(CHANGE_STATUS_SUCCESS); +export const changeStatusFailure = createAction(CHANGE_STATUS_FAILURE); + +export const changeRestaurant = createAction(CHANGE_RESTAURANT); +export const changeDate = createAction(CHANGE_DATE); + +export const loadProductsRequest = createAction(LOAD_PRODUCTS_REQUEST); +export const loadProductsSuccess = createAction(LOAD_PRODUCTS_SUCCESS); +export const loadProductsFailure = createAction(LOAD_PRODUCTS_FAILURE); + +export const loadProductOptionsSuccess = createAction( + LOAD_PRODUCT_OPTIONS_SUCCESS, +); + +export const setNextProductsPage = createAction(SET_NEXT_PRODUCTS_PAGE); +export const loadMoreProductsSuccess = createAction(LOAD_MORE_PRODUCTS_SUCCESS); +export const setHasMoreProducts = createAction(SET_HAS_MORE_PRODUCTS); + +export const changeProductEnabledRequest = createAction( + CHANGE_PRODUCT_ENABLED_REQUEST, + (product, enabled) => ({ product, enabled }), +); +export const changeProductEnabledSuccess = createAction( + CHANGE_PRODUCT_ENABLED_SUCCESS, +); +export const changeProductEnabledFailure = createAction( + CHANGE_PRODUCT_ENABLED_FAILURE, + (error, product, enabled) => ({ error, product, enabled }), +); + +export const changeProductOptionValueEnabledRequest = createAction( + CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST, + (productOptionValue, enabled) => ({ productOptionValue, enabled }), +); +export const changeProductOptionValueEnabledSuccess = createAction( + CHANGE_PRODUCT_OPTION_VALUE_ENABLED_SUCCESS, + (productOptionValue, enabled) => ({ productOptionValue, enabled }), +); +export const changeProductOptionValueEnabledFailure = createAction( + CHANGE_PRODUCT_OPTION_VALUE_ENABLED_FAILURE, + (error, productOptionValue, enabled) => ({ + error, + productOptionValue, + enabled, + }), +); + +export const closeRestaurantRequest = createAction(CLOSE_RESTAURANT_REQUEST); +export const closeRestaurantSuccess = createAction(CLOSE_RESTAURANT_SUCCESS); +export const closeRestaurantFailure = createAction(CLOSE_RESTAURANT_FAILURE); + +export const deleteOpeningHoursSpecificationRequest = createAction( + DELETE_OPENING_HOURS_SPECIFICATION_REQUEST, +); +export const deleteOpeningHoursSpecificationSuccess = createAction( + DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS, +); +export const deleteOpeningHoursSpecificationFailure = createAction( + DELETE_OPENING_HOURS_SPECIFICATION_FAILURE, +); + +export const printerConnected = createAction(PRINTER_CONNECTED); +export const printerDisconnected = createAction(PRINTER_DISCONNECTED); + +export const bluetoothEnabled = createAction(BLUETOOTH_ENABLED); +export const bluetoothDisabled = createAction(BLUETOOTH_DISABLED); +const _bluetoothStartScan = createAction(BLUETOOTH_START_SCAN); +export const bluetoothStopScan = createAction(BLUETOOTH_STOP_SCAN); +export const bluetoothStarted = createAction(BLUETOOTH_STARTED); + +export const sunmiPrinterDetected = createAction(SUNMI_PRINTER_DETECTED); + +export const setLoopeatFormats = createAction( + SET_LOOPEAT_FORMATS, + (order, loopeatFormats) => ({ order, loopeatFormats }), +); +export const updateLoopeatFormatsSuccess = createAction( + UPDATE_LOOPEAT_FORMATS_SUCCESS, +); /* * Thunk Creators */ export function loadMyRestaurants() { - return function (dispatch, getState) { + const httpClient = getState().app.httpClient; + dispatch(loadMyRestaurantsRequest()); - const httpClient = getState().app.httpClient - dispatch(loadMyRestaurantsRequest()) - - return httpClient.get('/api/me/restaurants') + return httpClient + .get('/api/me/restaurants') .then(res => dispatch(loadMyRestaurantsSuccess(res['hydra:member']))) - .catch(e => dispatch(loadMyRestaurantsFailure(e))) - } + .catch(e => dispatch(loadMyRestaurantsFailure(e))); + }; } export function loadOrders(restaurant, date, cb) { - return function (dispatch, getState) { + const httpClient = getState().app.httpClient; + dispatch(loadOrdersRequest()); - const httpClient = getState().app.httpClient - dispatch(loadOrdersRequest()) - - return httpClient.get(`${restaurant['@id']}/orders?date=${date}`) + return httpClient + .get(`${restaurant['@id']}/orders?date=${date}`) .then(res => { - dispatch(loadOrdersSuccess(res['hydra:member'])) + dispatch(loadOrdersSuccess(res['hydra:member'])); if (cb && typeof cb === 'function') { - cb(res) + cb(res); } }) - .catch(e => dispatch(loadOrdersFailure(e))) - } + .catch(e => dispatch(loadOrdersFailure(e))); + }; } export function loadMenus(restaurant, date) { - return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(loadMenusRequest()) + dispatch(loadMenusRequest()); - httpClient.get(`${restaurant['@id']}/menus`) + httpClient + .get(`${restaurant['@id']}/menus`) .then(res => dispatch(loadMenusSuccess(res['hydra:member']))) - .catch(e => dispatch(loadMenusFailure(e))) - } + .catch(e => dispatch(loadMenusFailure(e))); + }; } export function activateMenu(restaurant, menu) { - return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(loadMenusRequest()) + dispatch(loadMenusRequest()); - httpClient.put(`${restaurant['@id']}`, { - hasMenu: menu['@id'], - }) + httpClient + .put(`${restaurant['@id']}`, { + hasMenu: menu['@id'], + }) .then(res => dispatch(setCurrentMenu(restaurant, menu))) - .catch(e => dispatch(loadMenusFailure(e))) - } + .catch(e => dispatch(loadMenusFailure(e))); + }; } function gotoOrder(restaurant, order) { - NavigationHolder.dispatch(CommonActions.navigate({ - name: 'RestaurantNav', - params: { - screen: 'Main', + NavigationHolder.dispatch( + CommonActions.navigate({ + name: 'RestaurantNav', params: { - restaurant, - // We don't want to load orders again when navigating - loadOrders: false, - screen: 'RestaurantOrder', + screen: 'Main', params: { - order, + restaurant, + // We don't want to load orders again when navigating + loadOrders: false, + screen: 'RestaurantOrder', + params: { + order, + }, }, }, - }, - })) + }), + ); } export function loadOrder(order, cb) { - return function (dispatch, getState) { + const { app, restaurant } = getState(); + const { httpClient } = app; - const { app, restaurant } = getState() - const { httpClient } = app - - const sameOrder = _.find(restaurant.orders, o => o['@id'] === order) + const sameOrder = _.find(restaurant.orders, o => o['@id'] === order); // Optimization: don't reload the order if already loaded if (sameOrder) { // gotoOrder(sameOrder.restaurant, sameOrder) if (cb && typeof cb === 'function') { - setTimeout(() => cb(sameOrder), 0) + setTimeout(() => cb(sameOrder), 0); } - return + return; } - dispatch(loadOrderRequest()) + dispatch(loadOrderRequest()); - return httpClient.get(order) + return httpClient + .get(order) .then(res => { - dispatch(loadOrderSuccess(res)) + dispatch(loadOrderSuccess(res)); if (cb && typeof cb === 'function') { - setTimeout(() => cb(res), 0) + setTimeout(() => cb(res), 0); } }) .catch(e => { - dispatch(loadOrderFailure(e)) + dispatch(loadOrderFailure(e)); if (cb && typeof cb === 'function') { - setTimeout(() => cb(), 0) + setTimeout(() => cb(), 0); } - }) - } + }); + }; } export function loadOrderAndNavigate(order, cb) { - return function (dispatch, getState) { + const { app, restaurant } = getState(); + const { httpClient } = app; - const { app, restaurant } = getState() - const { httpClient } = app - - const sameOrder = _.find(restaurant.orders, o => o['@id'] === order) + const sameOrder = _.find(restaurant.orders, o => o['@id'] === order); // Optimization: don't reload the order if already loaded if (sameOrder) { - gotoOrder(sameOrder.restaurant, sameOrder) - return + gotoOrder(sameOrder.restaurant, sameOrder); + return; } - dispatch(loadOrderRequest()) + dispatch(loadOrderRequest()); - return httpClient.get(order) + return httpClient + .get(order) .then(res => { - - dispatch(loadOrderSuccess(res)) + dispatch(loadOrderSuccess(res)); if (cb && typeof cb === 'function') { - cb() + cb(); } - gotoOrder(res.restaurant, res) - + gotoOrder(res.restaurant, res); }) .catch(e => { - dispatch(loadOrderFailure(e)) + dispatch(loadOrderFailure(e)); if (cb && typeof cb === 'function') { - cb() + cb(); } - }) - } + }); + }; } export function loadOrderAndPushNotification(order) { - return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(loadOrderRequest()) + dispatch(loadOrderRequest()); - return httpClient.get(order) + return httpClient + .get(order) .then(res => { - dispatch(loadOrderSuccess(res)) - dispatch(pushNotification('order:created', { order: res })) + dispatch(loadOrderSuccess(res)); + dispatch(pushNotification('order:created', { order: res })); }) - .catch(e => dispatch(loadOrderFailure(e))) - } + .catch(e => dispatch(loadOrderFailure(e))); + }; } export function acceptOrder(order, cb) { - return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(acceptOrderRequest()) + dispatch(acceptOrderRequest()); - return httpClient.put(order['@id'] + '/accept') + return httpClient + .put(order['@id'] + '/accept') .then(res => { + dispatch(acceptOrderSuccess(res)); + + DropdownHolder.getDropdown().alertWithType( + 'success', + i18n.t('RESTAURANT_ORDER_ACCEPTED_CONFIRM_TITLE'), + i18n.t('RESTAURANT_ORDER_ACCEPTED_CONFIRM_BODY', { + number: order.number, + id: order.id, + }), + ); + + cb(res); + }) + .catch(e => dispatch(acceptOrderFailure(e))); + }; +} - dispatch(acceptOrderSuccess(res)) +export const startPreparing = createAsyncThunk( + 'order/startPreparing', + async (order, thunkAPI) => { + const { getState } = thunkAPI; - DropdownHolder - .getDropdown() - .alertWithType( - 'success', - i18n.t('RESTAURANT_ORDER_ACCEPTED_CONFIRM_TITLE'), - i18n.t('RESTAURANT_ORDER_ACCEPTED_CONFIRM_BODY', { number: order.number, id: order.id }) - ) + const httpClient = selectHttpClient(getState()); - cb(res) + const response = await httpClient.put(order['@id'] + '/start_preparing'); + return response.data; + }, +); - }) - .catch(e => dispatch(acceptOrderFailure(e))) - } -} +export const finishPreparing = createAsyncThunk( + 'order/finishPreparing', + async (order, thunkAPI) => { + const { getState } = thunkAPI; -export function refuseOrder(order, reason, cb) { + const httpClient = selectHttpClient(getState()); - return function (dispatch, getState) { + const response = await httpClient.put(order['@id'] + '/finish_preparing'); + return response.data; + }, +); - const { app } = getState() - const { httpClient } = app +export function refuseOrder(order, reason, cb) { + return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - dispatch(refuseOrderRequest()) + dispatch(refuseOrderRequest()); - return httpClient.put(order['@id'] + '/refuse', { reason }) + return httpClient + .put(order['@id'] + '/refuse', { reason }) .then(res => { - dispatch(refuseOrderSuccess(res)) - cb(res) + dispatch(refuseOrderSuccess(res)); + cb(res); }) - .catch(e => dispatch(refuseOrderFailure(e))) - } + .catch(e => dispatch(refuseOrderFailure(e))); + }; } export function delayOrder(order, delay, cb) { - return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(delayOrderRequest()) + dispatch(delayOrderRequest()); - return httpClient.put(order['@id'] + '/delay', { delay }) + return httpClient + .put(order['@id'] + '/delay', { delay }) .then(res => { - dispatch(delayOrderSuccess(res)) - cb(res) + dispatch(delayOrderSuccess(res)); + cb(res); }) - .catch(e => dispatch(delayOrderFailure(e))) - } + .catch(e => dispatch(delayOrderFailure(e))); + }; } export function fulfillOrder(order, cb) { - return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(fulfillOrderRequest()) + dispatch(fulfillOrderRequest()); - return httpClient.put(order['@id'] + '/fulfill', {}) + return httpClient + .put(order['@id'] + '/fulfill', {}) .then(res => { - dispatch(fulfillOrderSuccess(res)) + dispatch(fulfillOrderSuccess(res)); if (cb && typeof cb === 'function') { - cb(res) + cb(res); } }) - .catch(e => dispatch(fulfillOrderFailure(e))) - } + .catch(e => dispatch(fulfillOrderFailure(e))); + }; } export function cancelOrder(order, reason, cb) { - return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(cancelOrderRequest()) + dispatch(cancelOrderRequest()); - return httpClient.put(order['@id'] + '/cancel', { reason }) + return httpClient + .put(order['@id'] + '/cancel', { reason }) .then(res => { - - dispatch(cancelOrderSuccess(res)) - - DropdownHolder - .getDropdown() - .alertWithType( - 'success', - i18n.t('RESTAURANT_ORDER_CANCELLED_CONFIRM_TITLE'), - i18n.t('RESTAURANT_ORDER_CANCELLED_CONFIRM_BODY', { number: order.number, id: order.id }) - ) - - cb(res) - + dispatch(cancelOrderSuccess(res)); + + DropdownHolder.getDropdown().alertWithType( + 'success', + i18n.t('RESTAURANT_ORDER_CANCELLED_CONFIRM_TITLE'), + i18n.t('RESTAURANT_ORDER_CANCELLED_CONFIRM_BODY', { + number: order.number, + id: order.id, + }), + ); + + cb(res); }) - .catch(e => dispatch(cancelOrderFailure(e))) - } + .catch(e => dispatch(cancelOrderFailure(e))); + }; } export function changeStatus(restaurant, state) { - return (dispatch, getState) => { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(changeStatusRequest()) + dispatch(changeStatusRequest()); - return httpClient.put(restaurant['@id'], { state }) + return httpClient + .put(restaurant['@id'], { state }) .then(res => dispatch(changeStatusSuccess(res))) - .catch(e => dispatch(changeStatusFailure(e))) - } + .catch(e => dispatch(changeStatusFailure(e))); + }; } export function loadProducts(client, restaurant) { - return function (dispatch) { - dispatch(loadProductsRequest()) + dispatch(loadProductsRequest()); - return client.get(`${restaurant['@id']}/products`) + return client + .get(`${restaurant['@id']}/products`) .then(res => { - if (res.hasOwnProperty('hydra:view')) { - const hydraView = res['hydra:view'] + const hydraView = res['hydra:view']; if (hydraView.hasOwnProperty('hydra:next')) { - dispatch(setNextProductsPage(hydraView['hydra:next'])) - dispatch(setHasMoreProducts(true)) + dispatch(setNextProductsPage(hydraView['hydra:next'])); + dispatch(setHasMoreProducts(true)); } else { // It means we have reached the last page - dispatch(setHasMoreProducts(false)) + dispatch(setHasMoreProducts(false)); } } else { - dispatch(setHasMoreProducts(false)) + dispatch(setHasMoreProducts(false)); } - dispatch(loadProductsSuccess(res['hydra:member'])) - + dispatch(loadProductsSuccess(res['hydra:member'])); }) - .catch(e => dispatch(loadProductsFailure(e))) - } + .catch(e => dispatch(loadProductsFailure(e))); + }; } export function loadMoreProducts() { - return function (dispatch, getState) { - - const { httpClient } = getState().app - const { nextProductsPage, hasMoreProducts } = getState().restaurant + const { httpClient } = getState().app; + const { nextProductsPage, hasMoreProducts } = getState().restaurant; if (!hasMoreProducts) { - return + return; } - dispatch(loadProductsRequest()) + dispatch(loadProductsRequest()); - return httpClient.get(nextProductsPage) + return httpClient + .get(nextProductsPage) .then(res => { - - const hydraView = res['hydra:view'] + const hydraView = res['hydra:view']; if (hydraView.hasOwnProperty('hydra:next')) { - dispatch(setNextProductsPage(res['hydra:view']['hydra:next'])) - dispatch(setHasMoreProducts(true)) + dispatch(setNextProductsPage(res['hydra:view']['hydra:next'])); + dispatch(setHasMoreProducts(true)); } else { // It means we have reached the last page - dispatch(setHasMoreProducts(false)) + dispatch(setHasMoreProducts(false)); } - dispatch(loadMoreProductsSuccess(res['hydra:member'])) + dispatch(loadMoreProductsSuccess(res['hydra:member'])); }) - .catch(e => dispatch(loadProductsFailure(e))) - } + .catch(e => dispatch(loadProductsFailure(e))); + }; } export function changeProductEnabled(client, product, enabled) { - return function (dispatch) { - dispatch(changeProductEnabledRequest(product, enabled)) + dispatch(changeProductEnabledRequest(product, enabled)); - return client.put(product['@id'], { enabled }) + return client + .put(product['@id'], { enabled }) .then(res => dispatch(changeProductEnabledSuccess(res))) - .catch(e => dispatch(changeProductEnabledFailure(e, product, !enabled))) - } + .catch(e => dispatch(changeProductEnabledFailure(e, product, !enabled))); + }; } export function closeRestaurant(restaurant) { - return (dispatch, getState) => { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app + dispatch(closeRestaurantRequest()); - dispatch(closeRestaurantRequest()) - - return httpClient.put(`${restaurant['@id']}/close`, {}) + return httpClient + .put(`${restaurant['@id']}/close`, {}) .then(res => dispatch(closeRestaurantSuccess(res))) - .catch(e => dispatch(closeRestaurantFailure(e))) - } + .catch(e => dispatch(closeRestaurantFailure(e))); + }; } export function deleteOpeningHoursSpecification(openingHoursSpecification) { - return function (dispatch, getState) { - - const { app } = getState() - const { httpClient } = app - - dispatch(deleteOpeningHoursSpecificationRequest()) - - return httpClient.delete(openingHoursSpecification['@id']) - .then(res => dispatch(deleteOpeningHoursSpecificationSuccess(openingHoursSpecification))) - .catch(e => dispatch(deleteOpeningHoursSpecificationFailure(e))) - } + const { app } = getState(); + const { httpClient } = app; + + dispatch(deleteOpeningHoursSpecificationRequest()); + + return httpClient + .delete(openingHoursSpecification['@id']) + .then(res => + dispatch( + deleteOpeningHoursSpecificationSuccess(openingHoursSpecification), + ), + ) + .catch(e => dispatch(deleteOpeningHoursSpecificationFailure(e))); + }; } function bluetoothErrorToString(e) { if (typeof e === 'string') { - return e + return e; } - return e.message ? e.message : (e.toString && typeof e.toString === 'function' ? e.toString() : e) + return e.message + ? e.message + : e.toString && typeof e.toString === 'function' + ? e.toString() + : e; } export function printOrder(order) { - return async (dispatch, getState) => { - - const { printer, isSunmiPrinter } = getState().restaurant + const { printer, isSunmiPrinter } = getState().restaurant; try { - if (isSunmiPrinter) { - await SunmiPrinterLibrary.prepare() + await SunmiPrinterLibrary.prepare(); await SunmiPrinterLibrary.sendRAWData( - Buffer.from(encodeForPrinter(order, true)).toString('base64') - ) - return + Buffer.from(encodeForPrinter(order, true)).toString('base64'), + ); + return; } - } catch (e) { - console.log(e) + console.log(e); } if (!printer) { - return + return; } try { - - const isPeripheralConnected = await BleManager.isPeripheralConnected(printer.id, []) + const isPeripheralConnected = await BleManager.isPeripheralConnected( + printer.id, + [], + ); // Try to reconnect first if (!isPeripheralConnected) { try { - await BleManager.connect(printer.id) + await BleManager.connect(printer.id); } catch (e) { - dispatch(printerDisconnected()) - DropdownHolder - .getDropdown() - .alertWithType( - 'error', - i18n.t('RESTAURANT_PRINTER_CONNECT_ERROR_TITLE'), - bluetoothErrorToString(e) - ) - return + dispatch(printerDisconnected()); + DropdownHolder.getDropdown().alertWithType( + 'error', + i18n.t('RESTAURANT_PRINTER_CONNECT_ERROR_TITLE'), + bluetoothErrorToString(e), + ); + return; } } - const peripheralInfo = await BleManager.retrieveServices(printer.id) + const peripheralInfo = await BleManager.retrieveServices(printer.id); // We keep only writable characteristics - const writableCharacteristics = _.filter(peripheralInfo.characteristics, (characteristic) => { - - if (!characteristic.properties) { - return false - } + const writableCharacteristics = _.filter( + peripheralInfo.characteristics, + characteristic => { + if (!characteristic.properties) { + return false; + } - // iOS - if (Array.isArray(characteristic.properties)) { - return _.includes(characteristic.properties, 'WriteWithoutResponse') - } + // iOS + if (Array.isArray(characteristic.properties)) { + return _.includes( + characteristic.properties, + 'WriteWithoutResponse', + ); + } - // Android - return characteristic.properties.WriteWithoutResponse - }) + // Android + return characteristic.properties.WriteWithoutResponse; + }, + ); if (writableCharacteristics.length > 0) { - - const encoded = encodeForPrinter(order) + const encoded = encodeForPrinter(order); writableCharacteristics.sort((a, b) => { - if (peripheralInfo.advertising.serviceUUIDs) { - const isAdvertisedA = _.includes(peripheralInfo.advertising.serviceUUIDs, a.service) - const isAdvertisedB = _.includes(peripheralInfo.advertising.serviceUUIDs, b.service) + const isAdvertisedA = _.includes( + peripheralInfo.advertising.serviceUUIDs, + a.service, + ); + const isAdvertisedB = _.includes( + peripheralInfo.advertising.serviceUUIDs, + b.service, + ); if (isAdvertisedA !== isAdvertisedB) { if (isAdvertisedA && !isAdvertisedB) { - return -1 + return -1; } if (isAdvertisedB && !isAdvertisedA) { - return 1 + return 1; } } } if (a.service.length === b.service.length) { - return 0 + return 0; } - return a.service.length > b.service.length ? -1 : 1 - }) + return a.service.length > b.service.length ? -1 : 1; + }); for (let i = 0; i < writableCharacteristics.length; i++) { - - const writableCharacteristic = writableCharacteristics[i] + const writableCharacteristic = writableCharacteristics[i]; try { - await BleManager.writeWithoutResponse( printer.id, writableCharacteristic.service, writableCharacteristic.characteristic, - Array.from(encoded) - ) - - break + Array.from(encoded), + ); + break; } catch (e) { - console.log('Write failed', e) + console.log('Write failed', e); } } } - } catch (e) { - DropdownHolder - .getDropdown() - .alertWithType( - 'error', - i18n.t('RESTAURANT_PRINTER_CONNECT_ERROR_TITLE'), - bluetoothErrorToString(e) - ) + DropdownHolder.getDropdown().alertWithType( + 'error', + i18n.t('RESTAURANT_PRINTER_CONNECT_ERROR_TITLE'), + bluetoothErrorToString(e), + ); } - } + }; } export function connectPrinter(device, cb) { - return function (dispatch, getState) { BleManager.connect(device.id) .then(() => { + dispatch(printerConnected(device)); - dispatch(printerConnected(device)) - - DropdownHolder - .getDropdown() - .alertWithType( - 'success', - i18n.t('RESTAURANT_PRINTER_CONNECTED_TITLE'), - i18n.t('RESTAURANT_PRINTER_CONNECTED_BODY', { name: (device.name || device.id) }) - ) - - cb && cb() + DropdownHolder.getDropdown().alertWithType( + 'success', + i18n.t('RESTAURANT_PRINTER_CONNECTED_TITLE'), + i18n.t('RESTAURANT_PRINTER_CONNECTED_BODY', { + name: device.name || device.id, + }), + ); + cb && cb(); }) .catch(e => { - DropdownHolder - .getDropdown() - .alertWithType( - 'error', - i18n.t('RESTAURANT_PRINTER_CONNECT_ERROR_TITLE'), - bluetoothErrorToString(e) - ) - }) - } + DropdownHolder.getDropdown().alertWithType( + 'error', + i18n.t('RESTAURANT_PRINTER_CONNECT_ERROR_TITLE'), + bluetoothErrorToString(e), + ); + }); + }; } export function disconnectPrinter(device, cb) { - return function (dispatch, getState) { BleManager.disconnect(device.id) // We use Promose.finally because if the state @@ -770,102 +819,108 @@ export function disconnectPrinter(device, cb) { // BleManager.disconnect will return an error .catch(e => console.log(e)) .finally(() => { - - dispatch(printerDisconnected()) - - DropdownHolder - .getDropdown() - .alertWithType( - 'success', - i18n.t('RESTAURANT_PRINTER_DISCONNECTED_TITLE'), - i18n.t('RESTAURANT_PRINTER_DISCONNECTED_BODY', { name: (device.name || device.id) }) - ) - - cb && cb() - - }) - } + dispatch(printerDisconnected()); + + DropdownHolder.getDropdown().alertWithType( + 'success', + i18n.t('RESTAURANT_PRINTER_DISCONNECTED_TITLE'), + i18n.t('RESTAURANT_PRINTER_DISCONNECTED_BODY', { + name: device.name || device.id, + }), + ); + + cb && cb(); + }); + }; } export function loadProductOptions(restaurant) { - return function (dispatch, getState) { + const { app } = getState(); + const { httpClient } = app; - const { app } = getState() - const { httpClient } = app - - dispatch(loadProductsRequest()) + dispatch(loadProductsRequest()); - return httpClient.get(`${restaurant['@id']}/product_options`) + return httpClient + .get(`${restaurant['@id']}/product_options`) .then(res => { - dispatch(loadProductOptionsSuccess(res['hydra:member'])) + dispatch(loadProductOptionsSuccess(res['hydra:member'])); }) - .catch(e => dispatch(loadProductsFailure(e))) - } + .catch(e => dispatch(loadProductsFailure(e))); + }; } export function changeProductOptionValueEnabled(productOptionValue, enabled) { - return function (dispatch, getState) { - - const { app } = getState() - const { httpClient } = app - - dispatch(changeProductOptionValueEnabledRequest(productOptionValue, enabled)) - - httpClient.put(productOptionValue['@id'], { enabled }) - .then(res => dispatch(changeProductOptionValueEnabledSuccess(productOptionValue, res.enabled))) - .catch(e => dispatch(changeProductOptionValueEnabledSuccess(e, productOptionValue, !enabled))) - } + const { app } = getState(); + const { httpClient } = app; + + dispatch( + changeProductOptionValueEnabledRequest(productOptionValue, enabled), + ); + + httpClient + .put(productOptionValue['@id'], { enabled }) + .then(res => + dispatch( + changeProductOptionValueEnabledSuccess( + productOptionValue, + res.enabled, + ), + ), + ) + .catch(e => + dispatch( + changeProductOptionValueEnabledSuccess( + e, + productOptionValue, + !enabled, + ), + ), + ); + }; } export function bluetoothStartScan() { - return function (dispatch, getState) { - if (!getState().restaurant.bluetoothStarted) { - BleManager.start({ showAlert: false }) - .then(() => { - dispatch(bluetoothStarted()) + BleManager.start({ showAlert: false }).then(() => { + dispatch(bluetoothStarted()); BleManager.scan([], 30, true).then(() => { - dispatch(_bluetoothStartScan()) - }) - }) + dispatch(_bluetoothStartScan()); + }); + }); } else { BleManager.scan([], 30, true).then(() => { - dispatch(_bluetoothStartScan()) - }) + dispatch(_bluetoothStartScan()); + }); } - } + }; } export function loadLoopeatFormats(order) { - return function (dispatch, getState) { + const { httpClient } = getState().app; - const { httpClient } = getState().app - - httpClient.get(order['@id'] + '/loopeat_formats') - .then(res => { - dispatch(setLoopeatFormats(order, res.items)) - }) - } + httpClient.get(order['@id'] + '/loopeat_formats').then(res => { + dispatch(setLoopeatFormats(order, res.items)); + }); + }; } export function updateLoopeatFormats(order, loopeatFormats, cb) { - return (dispatch, getState) => { + const { httpClient } = getState().app; - const { httpClient } = getState().app - - httpClient.put(order['@id'] + '/loopeat_formats', { - items: loopeatFormats, - }) + httpClient + .put(order['@id'] + '/loopeat_formats', { + items: loopeatFormats, + }) .then(res => { - dispatch(updateLoopeatFormatsSuccess(res)) + dispatch(updateLoopeatFormatsSuccess(res)); if (cb && typeof cb === 'function') { - cb(res) + cb(res); } - }) - } + }); + }; } diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index 167cbb6c9..3e17f6469 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -3,6 +3,7 @@ import { find } from 'lodash'; import moment from 'moment'; import _ from 'lodash'; import { matchesDate } from '../../utils/order'; +import { STATE } from '../../model/Order'; export const selectRestaurant = state => state.restaurant.restaurant; export const selectDate = state => state.restaurant.date; @@ -45,7 +46,7 @@ export const selectNewOrders = createSelector( _selectOrders, (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && o.state === 'new'), + _.filter(orders, o => matchesDate(o, date) && o.state === STATE.NEW), [o => moment.parseZone(o.pickupExpectedAt)], ), ); @@ -55,7 +56,7 @@ export const selectAcceptedOrders = createSelector( _selectOrders, (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && o.state === 'accepted'), + _.filter(orders, o => matchesDate(o, date) && o.state === STATE.ACCEPTED), [o => moment.parseZone(o.pickupExpectedAt)], ), ); @@ -65,7 +66,7 @@ export const selectStartedOrders = createSelector( _selectOrders, (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && o.state === 'started'), + _.filter(orders, o => matchesDate(o, date) && o.state === STATE.STARTED), [o => moment.parseZone(o.pickupExpectedAt)], ), ); @@ -77,7 +78,7 @@ export const selectReadyOrders = createSelector( _.sortBy( _.filter( orders, - o => matchesDate(o, date) && o.state === 'ready' && !o.assignedTo, + o => matchesDate(o, date) && o.state === STATE.READY && !o.assignedTo, ), [o => moment.parseZone(o.pickupExpectedAt)], ), @@ -90,7 +91,7 @@ export const selectPickedOrders = createSelector( _.sortBy( _.filter( orders, - o => matchesDate(o, date) && o.state === 'ready' && !!o.assignedTo, + o => matchesDate(o, date) && o.state === STATE.READY && !!o.assignedTo, ), [o => moment.parseZone(o.pickupExpectedAt)], ), @@ -105,7 +106,7 @@ export const selectCancelledOrders = createSelector( orders, o => matchesDate(o, date) && - (o.state === 'refused' || o.state === 'cancelled'), + (o.state === STATE.REFUSED || o.state === STATE.CANCELLED), ), [o => moment.parseZone(o.pickupExpectedAt)], ), @@ -115,5 +116,5 @@ export const selectFulfilledOrders = createSelector( selectDate, _selectOrders, (date, orders) => - _.filter(orders, o => matchesDate(o, date) && o.state === 'fulfilled'), + _.filter(orders, o => matchesDate(o, date) && o.state === STATE.FULFILLED), ); From 7b755097740fd35af649aeff9e68540aeb4e82b4 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:34:17 -0700 Subject: [PATCH 09/69] green button to move order to the next state --- .../restaurant/components/OrderItem.js | 2 +- src/redux/Restaurant/actions.js | 12 +- src/redux/Restaurant/reducers.js | 243 +++++++++--------- 3 files changed, 133 insertions(+), 124 deletions(-) diff --git a/src/navigation/restaurant/components/OrderItem.js b/src/navigation/restaurant/components/OrderItem.js index 29cde403c..ff38ff552 100644 --- a/src/navigation/restaurant/components/OrderItem.js +++ b/src/navigation/restaurant/components/OrderItem.js @@ -11,8 +11,8 @@ import React from 'react'; import { useDispatch } from 'react-redux'; import { acceptOrder, - startPreparing, finishPreparing, + startPreparing, } from '../../../redux/Restaurant/actions'; import { STATE } from '../../../model/Order'; diff --git a/src/redux/Restaurant/actions.js b/src/redux/Restaurant/actions.js index 18abbde17..c4e0aaa1b 100644 --- a/src/redux/Restaurant/actions.js +++ b/src/redux/Restaurant/actions.js @@ -441,8 +441,7 @@ export const startPreparing = createAsyncThunk( const httpClient = selectHttpClient(getState()); - const response = await httpClient.put(order['@id'] + '/start_preparing'); - return response.data; + return await httpClient.put(order['@id'] + '/start_preparing'); }, ); @@ -453,8 +452,7 @@ export const finishPreparing = createAsyncThunk( const httpClient = selectHttpClient(getState()); - const response = await httpClient.put(order['@id'] + '/finish_preparing'); - return response.data; + return await httpClient.put(order['@id'] + '/finish_preparing'); }, ); @@ -786,7 +784,7 @@ export function printOrder(order) { } export function connectPrinter(device, cb) { - return function (dispatch, getState) { + return function (dispatch) { BleManager.connect(device.id) .then(() => { dispatch(printerConnected(device)); @@ -812,9 +810,9 @@ export function connectPrinter(device, cb) { } export function disconnectPrinter(device, cb) { - return function (dispatch, getState) { + return function (dispatch) { BleManager.disconnect(device.id) - // We use Promose.finally because if the state + // We use Promise.finally because if the state // contains a saved printer which is not connected anymore, // BleManager.disconnect will return an error .catch(e => console.log(e)) diff --git a/src/redux/Restaurant/reducers.js b/src/redux/Restaurant/reducers.js index ae80ccac1..19ad67369 100644 --- a/src/redux/Restaurant/reducers.js +++ b/src/redux/Restaurant/reducers.js @@ -4,6 +4,7 @@ import { ACCEPT_ORDER_SUCCESS, BLUETOOTH_DISABLED, BLUETOOTH_ENABLED, + BLUETOOTH_STARTED, BLUETOOTH_START_SCAN, BLUETOOTH_STOP_SCAN, CANCEL_ORDER_FAILURE, @@ -53,30 +54,29 @@ import { REFUSE_ORDER_SUCCESS, SET_CURRENT_MENU, SET_HAS_MORE_PRODUCTS, + SET_LOOPEAT_FORMATS, SET_NEXT_PRODUCTS_PAGE, SUNMI_PRINTER_DETECTED, - BLUETOOTH_STARTED, - SET_LOOPEAT_FORMATS, UPDATE_LOOPEAT_FORMATS_SUCCESS, -} from './actions' + finishPreparing, + startPreparing, +} from './actions'; import { LOAD_MY_RESTAURANTS_FAILURE, LOAD_MY_RESTAURANTS_REQUEST, LOAD_MY_RESTAURANTS_SUCCESS, -} from '../App/actions' +} from '../App/actions'; -import { - MESSAGE, -} from '../middlewares/CentrifugoMiddleware/actions' +import { MESSAGE } from '../middlewares/CentrifugoMiddleware/actions'; -import moment from 'moment' -import _ from 'lodash' +import moment from 'moment'; +import _ from 'lodash'; const initialState = { - fetchError: null, // Error object describing the error + fetchError: null, // Error object describing the error isFetching: false, // Flag indicating active HTTP request - orders: [], // Array of orders + orders: [], // Array of orders myRestaurants: [], // Array of restaurants date: moment(), status: 'available', @@ -92,73 +92,88 @@ const initialState = { isSunmiPrinter: false, bluetoothStarted: false, loopeatFormats: {}, -} +}; const spliceOrders = (state, payload) => { - - const orderIndex = _.findIndex(state.orders, order => order['@id'] === payload['@id']) + const orderIndex = _.findIndex( + state.orders, + order => order['@id'] === payload['@id'], + ); if (orderIndex !== -1) { - const newOrders = state.orders.slice(0) - newOrders.splice(orderIndex, 1, Object.assign({}, payload)) + const newOrders = state.orders.slice(0); + newOrders.splice(orderIndex, 1, Object.assign({}, payload)); - return newOrders + return newOrders; } - return state.orders -} + return state.orders; +}; const addOrReplace = (state, payload) => { + const newOrders = state.orders.slice(0); - const newOrders = state.orders.slice(0) - - const orderIndex = _.findIndex(state.orders, o => o['@id'] === payload['@id']) + const orderIndex = _.findIndex( + state.orders, + o => o['@id'] === payload['@id'], + ); if (orderIndex !== -1) { - newOrders.splice(orderIndex, 1, { ...payload }) + newOrders.splice(orderIndex, 1, { ...payload }); - return newOrders + return newOrders; } - return newOrders.concat([payload]) -} + return newOrders.concat([payload]); +}; const spliceProducts = (state, payload) => { - - const productIndex = _.findIndex(state.products, product => product['@id'] === payload['@id']) + const productIndex = _.findIndex( + state.products, + product => product['@id'] === payload['@id'], + ); if (productIndex !== -1) { - const newProducts = state.products.slice(0) - newProducts.splice(productIndex, 1, Object.assign({}, payload)) + const newProducts = state.products.slice(0); + newProducts.splice(productIndex, 1, Object.assign({}, payload)); - return newProducts + return newProducts; } - return state.products -} + return state.products; +}; const spliceProductOptions = (state, payload) => { - const productOptionIndex = _.findIndex(state, productOption => { - return _.findIndex(productOption.values, productOptionValue => productOptionValue['@id'] === payload.productOptionValue['@id']) !== -1 - }) + return ( + _.findIndex( + productOption.values, + productOptionValue => + productOptionValue['@id'] === payload.productOptionValue['@id'], + ) !== -1 + ); + }); if (productOptionIndex !== -1) { + const newProductOptions = state.slice(); - const newProductOptions = state.slice() + const productOptionValueIndex = _.findIndex( + state[productOptionIndex].values, + productOptionValue => + productOptionValue['@id'] === payload.productOptionValue['@id'], + ); - const productOptionValueIndex = - _.findIndex(state[productOptionIndex].values, productOptionValue => productOptionValue['@id'] === payload.productOptionValue['@id']) + newProductOptions[productOptionIndex].values[ + productOptionValueIndex + ].enabled = payload.enabled; - newProductOptions[productOptionIndex].values[productOptionValueIndex].enabled = payload.enabled - - return newProductOptions + return newProductOptions; } - return state -} + return state; +}; export default (state = initialState, action = {}) => { - let newState + let newState; switch (action.type) { case LOAD_ORDERS_REQUEST: @@ -169,6 +184,8 @@ export default (state = initialState, action = {}) => { case DELAY_ORDER_REQUEST: case FULFILL_ORDER_REQUEST: case CANCEL_ORDER_REQUEST: + case startPreparing.pending.type: + case finishPreparing.pending.type: case CHANGE_STATUS_REQUEST: case LOAD_PRODUCTS_REQUEST: case CLOSE_RESTAURANT_REQUEST: @@ -178,7 +195,7 @@ export default (state = initialState, action = {}) => { ...state, fetchError: false, isFetching: true, - } + }; case LOAD_ORDERS_FAILURE: case LOAD_ORDER_FAILURE: @@ -188,6 +205,8 @@ export default (state = initialState, action = {}) => { case DELAY_ORDER_FAILURE: case FULFILL_ORDER_FAILURE: case CANCEL_ORDER_FAILURE: + case startPreparing.rejected.type: + case finishPreparing.rejected.type: case CHANGE_STATUS_FAILURE: case LOAD_PRODUCTS_FAILURE: case CLOSE_RESTAURANT_FAILURE: @@ -197,7 +216,7 @@ export default (state = initialState, action = {}) => { ...state, fetchError: action.payload || action.error, isFetching: false, - } + }; case CHANGE_PRODUCT_ENABLED_REQUEST: return { @@ -208,7 +227,7 @@ export default (state = initialState, action = {}) => { ...action.payload.product, enabled: action.payload.enabled, }), - } + }; case CHANGE_PRODUCT_ENABLED_FAILURE: return { @@ -219,7 +238,7 @@ export default (state = initialState, action = {}) => { ...action.payload.product, enabled: action.payload.enabled, }), - } + }; case CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST: case CHANGE_PRODUCT_OPTION_VALUE_ENABLED_SUCCESS: @@ -227,16 +246,22 @@ export default (state = initialState, action = {}) => { ...state, fetchError: false, isFetching: action.type === CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST, - productOptions: spliceProductOptions(state.productOptions, action.payload), - } + productOptions: spliceProductOptions( + state.productOptions, + action.payload, + ), + }; case CHANGE_PRODUCT_OPTION_VALUE_ENABLED_FAILURE: return { ...state, fetchError: action.payload.error, isFetching: false, - productOptions: spliceProductOptions(state.productOptions, action.payload), - } + productOptions: spliceProductOptions( + state.productOptions, + action.payload, + ), + }; case LOAD_ORDERS_SUCCESS: return { @@ -244,7 +269,7 @@ export default (state = initialState, action = {}) => { fetchError: false, isFetching: false, orders: action.payload, - } + }; case LOAD_ORDER_SUCCESS: return { @@ -252,41 +277,42 @@ export default (state = initialState, action = {}) => { fetchError: false, isFetching: false, orders: addOrReplace(state, action.payload), - } + }; case ACCEPT_ORDER_SUCCESS: case REFUSE_ORDER_SUCCESS: case DELAY_ORDER_SUCCESS: case FULFILL_ORDER_SUCCESS: case CANCEL_ORDER_SUCCESS: + case startPreparing.fulfilled.type: + case finishPreparing.fulfilled.type: return { ...state, orders: spliceOrders(state, action.payload), fetchError: false, isFetching: false, - } + }; case LOAD_MY_RESTAURANTS_SUCCESS: - newState = { ...state, fetchError: false, isFetching: false, myRestaurants: action.payload, - } + }; if (action.payload.length > 0) { - const restaurant = _.first(action.payload) + const restaurant = _.first(action.payload); newState = { ...newState, // We select by default the first restaurant from the list // Most of the time, users will own only one restaurant restaurant, - } + }; } - return newState + return newState; case LOAD_PRODUCTS_SUCCESS: return { @@ -294,7 +320,7 @@ export default (state = initialState, action = {}) => { fetchError: false, isFetching: false, products: action.payload, - } + }; case LOAD_PRODUCT_OPTIONS_SUCCESS: return { @@ -302,7 +328,7 @@ export default (state = initialState, action = {}) => { fetchError: false, isFetching: false, productOptions: action.payload, - } + }; case LOAD_MORE_PRODUCTS_SUCCESS: return { @@ -310,7 +336,7 @@ export default (state = initialState, action = {}) => { fetchError: false, isFetching: false, products: state.products.concat(action.payload), - } + }; case CHANGE_PRODUCT_ENABLED_SUCCESS: return { @@ -318,20 +344,18 @@ export default (state = initialState, action = {}) => { fetchError: false, isFetching: false, products: spliceProducts(state, action.payload), - } + }; case CLOSE_RESTAURANT_SUCCESS: - return { ...state, fetchError: false, isFetching: false, restaurant: action.payload, - } - - case DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS: + }; - const { specialOpeningHoursSpecification } = state + case DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS: { + const { specialOpeningHoursSpecification } = state; return { ...state, @@ -341,10 +365,12 @@ export default (state = initialState, action = {}) => { ...state.restaurant, specialOpeningHoursSpecification: _.filter( specialOpeningHoursSpecification, - openingHoursSpecification => openingHoursSpecification['@id'] !== action.payload['@id'] + openingHoursSpecification => + openingHoursSpecification['@id'] !== action.payload['@id'], ), }, - } + }; + } case CHANGE_STATUS_SUCCESS: return { @@ -352,35 +378,33 @@ export default (state = initialState, action = {}) => { fetchError: false, isFetching: false, restaurant: action.payload, - } + }; case CHANGE_RESTAURANT: - return { ...state, restaurant: action.payload, - } + }; case CHANGE_DATE: return { ...state, date: action.payload, - } + }; case SET_NEXT_PRODUCTS_PAGE: return { ...state, nextProductsPage: action.payload, - } + }; case SET_HAS_MORE_PRODUCTS: return { ...state, hasMoreProducts: action.payload, - } + }; case LOAD_MENUS_SUCCESS: - return { ...state, fetchError: false, @@ -389,10 +413,9 @@ export default (state = initialState, action = {}) => { ...menu, active: menu['@id'] === state.restaurant.hasMenu, })), - } + }; case SET_CURRENT_MENU: - return { ...state, fetchError: false, @@ -405,121 +428,109 @@ export default (state = initialState, action = {}) => { ...menu, active: menu['@id'] === action.payload.menu['@id'], })), - } + }; case PRINTER_CONNECTED: - return { ...state, printer: action.payload, - } + }; case PRINTER_DISCONNECTED: - return { ...state, printer: null, - } + }; case BLUETOOTH_ENABLED: - return { ...state, bluetoothEnabled: true, - } + }; case BLUETOOTH_DISABLED: - return { ...state, bluetoothEnabled: false, - } + }; case BLUETOOTH_START_SCAN: - return { ...state, isScanningBluetooth: true, - } + }; case BLUETOOTH_STOP_SCAN: - return { ...state, isScanningBluetooth: false, - } + }; case SUNMI_PRINTER_DETECTED: - return { ...state, isSunmiPrinter: true, - } + }; case MESSAGE: - if (action.payload.name && action.payload.data) { - - const { name, data } = action.payload + const { name, data } = action.payload; switch (name) { case 'order:created': case 'order:accepted': case 'order:picked': - case 'order:cancelled': - + case 'order:cancelled': { // FIXME // Fix this on API side - let newOrder = { ...data.order } + let newOrder = { ...data.order }; if (name === 'order:cancelled' && newOrder.state !== 'cancelled') { newOrder = { ...newOrder, state: 'cancelled', - } + }; } if (name === 'order:accepted' && newOrder.state !== 'accepted') { newOrder = { ...newOrder, state: 'accepted', - } + }; } return { ...state, orders: addOrReplace(state, newOrder), - } + }; + } default: - break + break; } } - return state + return state; case BLUETOOTH_STARTED: - return { ...state, bluetoothStarted: true, - } + }; case SET_LOOPEAT_FORMATS: - return { ...state, loopeatFormats: { ...state.loopeatFormats, [action.payload.order['@id']]: action.payload.loopeatFormats, - } - } + }, + }; case UPDATE_LOOPEAT_FORMATS_SUCCESS: - return { ...state, isFetching: false, orders: addOrReplace(state, action.payload), - } + }; } - return state -} + return state; +}; From 78f1a3a4060deb6416cfb1f9a4e9cca808abd4e1 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:35:16 -0700 Subject: [PATCH 10/69] renamed model -> domain --- src/{model => domain}/Order.js | 0 src/navigation/restaurant/components/OrderItem.js | 2 +- src/redux/Restaurant/selectors.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{model => domain}/Order.js (100%) diff --git a/src/model/Order.js b/src/domain/Order.js similarity index 100% rename from src/model/Order.js rename to src/domain/Order.js diff --git a/src/navigation/restaurant/components/OrderItem.js b/src/navigation/restaurant/components/OrderItem.js index ff38ff552..d0253b26c 100644 --- a/src/navigation/restaurant/components/OrderItem.js +++ b/src/navigation/restaurant/components/OrderItem.js @@ -14,7 +14,7 @@ import { finishPreparing, startPreparing, } from '../../../redux/Restaurant/actions'; -import { STATE } from '../../../model/Order'; +import { STATE } from '../../../domain/Order'; const styles = StyleSheet.create({ item: { diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index 3e17f6469..f7e7ff21e 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -3,7 +3,7 @@ import { find } from 'lodash'; import moment from 'moment'; import _ from 'lodash'; import { matchesDate } from '../../utils/order'; -import { STATE } from '../../model/Order'; +import { STATE } from '../../domain/Order'; export const selectRestaurant = state => state.restaurant.restaurant; export const selectDate = state => state.restaurant.date; From c8b59d6035068b0021e5919c2d23af681787922f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:12:02 -0700 Subject: [PATCH 11/69] use new 'order:state_changed' event to get order updates --- src/redux/Restaurant/reducers.js | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/redux/Restaurant/reducers.js b/src/redux/Restaurant/reducers.js index 19ad67369..aa5bd1f5f 100644 --- a/src/redux/Restaurant/reducers.js +++ b/src/redux/Restaurant/reducers.js @@ -478,30 +478,12 @@ export default (state = initialState, action = {}) => { switch (name) { case 'order:created': - case 'order:accepted': case 'order:picked': - case 'order:cancelled': { - // FIXME - // Fix this on API side - let newOrder = { ...data.order }; - if (name === 'order:cancelled' && newOrder.state !== 'cancelled') { - newOrder = { - ...newOrder, - state: 'cancelled', - }; - } - if (name === 'order:accepted' && newOrder.state !== 'accepted') { - newOrder = { - ...newOrder, - state: 'accepted', - }; - } - + case 'order:state_changed': return { ...state, - orders: addOrReplace(state, newOrder), + orders: addOrReplace(state, data.order), }; - } default: break; } From 4417e36f549e0b60ffcd23785fbfcb0333ef0a58 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:17:10 -0700 Subject: [PATCH 12/69] fixed: section header background --- .../restaurant/components/OrderList.js | 24 +++++++------------ .../{OrderItem.js => OrderListItem.js} | 2 +- .../components/OrderListSectionHeader.js | 19 +++++++++++++++ 3 files changed, 28 insertions(+), 17 deletions(-) rename src/navigation/restaurant/components/{OrderItem.js => OrderListItem.js} (97%) create mode 100644 src/navigation/restaurant/components/OrderListSectionHeader.js diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index 0a0e3a019..7603a6c4b 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -1,6 +1,5 @@ import React from 'react'; -import { SectionList, StyleSheet, View } from 'react-native'; -import { Text } from 'native-base'; +import { SectionList } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { @@ -13,15 +12,9 @@ import { selectRestaurant, selectStartedOrders, } from '../../../redux/Restaurant/selectors'; -import OrderItem from './OrderItem'; - -const styles = StyleSheet.create({ - sectionHeader: { - paddingTop: 32, - paddingBottom: 12, - paddingHorizontal: 24, - }, -}); +import OrderListItem from './OrderListItem'; +import OrderListSectionHeader from './OrderListSectionHeader'; +import { View } from 'native-base'; export default function OrderList({ onItemClick }) { const restaurant = useSelector(selectRestaurant); @@ -87,16 +80,15 @@ export default function OrderList({ onItemClick }) { return ( item['@id']} + keyExtractor={item => item['@id']} sections={sections} renderSectionHeader={({ section: { title } }) => ( - - {title} - + )} renderItem={({ item }) => ( - + )} + renderSectionFooter={() => } /> ); } diff --git a/src/navigation/restaurant/components/OrderItem.js b/src/navigation/restaurant/components/OrderListItem.js similarity index 97% rename from src/navigation/restaurant/components/OrderItem.js rename to src/navigation/restaurant/components/OrderListItem.js index d0253b26c..2ab482db5 100644 --- a/src/navigation/restaurant/components/OrderItem.js +++ b/src/navigation/restaurant/components/OrderListItem.js @@ -47,7 +47,7 @@ const styles = StyleSheet.create({ }, }); -export default function OrderItem({ order, onItemClick }) { +export default function OrderListItem({ order, onItemClick }) { const dispatch = useDispatch(); const isActionable = [STATE.NEW, STATE.ACCEPTED, STATE.STARTED].includes( diff --git a/src/navigation/restaurant/components/OrderListSectionHeader.js b/src/navigation/restaurant/components/OrderListSectionHeader.js new file mode 100644 index 000000000..22a4d185f --- /dev/null +++ b/src/navigation/restaurant/components/OrderListSectionHeader.js @@ -0,0 +1,19 @@ +import { Text } from 'native-base'; +import { View } from 'react-native'; +import React from 'react'; +import { useBackgroundColor } from '../../../styles/theme'; + +export default function OrderListSectionHeader({ title }) { + const backgroundColor = useBackgroundColor(); + + return ( + + {title} + + ); +} From 9f1a722f559899c465f247820a76d0c0ff0fc96f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:55:07 -0700 Subject: [PATCH 13/69] refactor NotificationHandler --- src/components/NotificationHandler.js | 404 +++----------------------- src/components/NotificationModal.js | 173 +++++++++++ src/hooks/usePlayNotificationSound.js | 76 +++++ src/hooks/usePushNotification.js | 114 ++++++++ src/redux/App/selectors.js | 144 +++++---- 5 files changed, 479 insertions(+), 432 deletions(-) create mode 100644 src/components/NotificationModal.js create mode 100644 src/hooks/usePlayNotificationSound.js create mode 100644 src/hooks/usePushNotification.js diff --git a/src/components/NotificationHandler.js b/src/components/NotificationHandler.js index e66a20148..44dba0fb1 100644 --- a/src/components/NotificationHandler.js +++ b/src/components/NotificationHandler.js @@ -1,383 +1,47 @@ -import React, { Component } from 'react'; -import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native'; -import { Icon, Text } from 'native-base'; -import { connect } from 'react-redux'; -import { withTranslation } from 'react-i18next'; -import Sound from 'react-native-sound'; -import moment from 'moment'; -import Modal from 'react-native-modal'; -import { CommonActions } from '@react-navigation/native'; -import FontAwesome from 'react-native-vector-icons/FontAwesome'; -import Ionicons from 'react-native-vector-icons/Ionicons'; +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; -import PushNotification from '../notifications'; -import NavigationHolder from '../NavigationHolder'; - -import { - clearNotifications, - pushNotification, - registerPushNotificationToken, -} from '../redux/App/actions'; -import { loadTasks, selectTasksChangedAlertSound } from '../redux/Courier'; -import { - loadOrder, - loadOrderAndNavigate, - loadOrderAndPushNotification, -} from '../redux/Restaurant/actions'; -import { message as wsMessage } from '../redux/middlewares/CentrifugoMiddleware/actions'; -import tracker from '../analytics/Tracker'; -import analyticsEvent from '../analytics/Event'; - -import ModalContent from './ModalContent'; - -// Make sure sound will play even when device is in silent mode -Sound.setCategory('Playback'); +import { clearNotifications } from '../redux/App/actions'; +import NotificationModal from './NotificationModal'; +import usePushNotification from '../hooks/usePushNotification'; +import usePlayNotificationSound from '../hooks/usePlayNotificationSound'; +import { selectNotifications } from '../redux/App/selectors'; /** * This component is used - * 1/ To configure push notifications (see componentDidMount) - * 2/ To show notifications when the app is in foreground. + * 1/ To configure push notifications (see usePushNotification) + * 2/ To show notifications when the app is in foreground (see NotificationModal) + * 3/ To play a sound when a notification is received (see usePlayNotificationSound) */ -class NotificationHandler extends Component { - constructor(props) { - super(props); - - this.state = { - sound: null, - isSoundReady: false, - }; - } - - _onTasksChanged(date) { - if (this.props.currentRoute !== 'CourierTaskList') { - NavigationHolder.navigate('CourierTaskList', {}); - } +export default function NotificationHandler() { + usePushNotification(); - this.props.loadTasks(moment(date)); - } + const notifications = useSelector(selectNotifications); - _loadSound() { - const bell = new Sound( - 'misstickle__indian_bell_chime.wav', - Sound.MAIN_BUNDLE, - error => { - if (error) { - return; - } + const { isSoundPlaying, stopSound } = usePlayNotificationSound(notifications); - bell.setNumberOfLoops(-1); + const dispatch = useDispatch(); - this.setState({ - sound: bell, - isSoundReady: true, - }); - }, - ); - } + const clear = useCallback(() => { + stopSound(); + dispatch(clearNotifications()); + }, [stopSound, dispatch]); - _startSound() { - const { sound, isSoundReady } = this.state; - if (isSoundReady) { - sound.play(success => { - if (!success) { - sound.reset(); - } - }); + useEffect(() => { + if (isSoundPlaying) { // Clear notifications after 10 seconds - setTimeout(() => this.props.clearNotifications(), 10000); - } - } - - _stopSound() { - const { sound, isSoundReady } = this.state; - if (isSoundReady) { - sound.stop(() => {}); - } - } - - includesNotification(notifications, predicate) { - return notifications.findIndex(predicate) !== -1; - } - - componentDidUpdate(prevProps) { - if (this.props.notifications !== prevProps.notifications) { - if (this.props.notifications.length > 0) { - if ( - this.includesNotification( - this.props.notifications, - n => n.event === 'order:created', - ) - ) { - this._startSound(); - } else if ( - this.includesNotification( - this.props.notifications, - n => n.event === 'tasks:changed', - ) - ) { - if (this.props.tasksChangedAlertSound) { - this._startSound(); - } - } - } else { - this._stopSound(); - } - } - } - - componentDidMount() { - this._loadSound(); - - PushNotification.configure({ - onRegister: token => this.props.registerPushNotificationToken(token), - onNotification: message => { - const { event } = message.data; - - if (event && event.name === 'order:created') { - tracker.logEvent( - analyticsEvent.restaurant._category, - analyticsEvent.restaurant.orderCreatedMessage, - message.foreground ? 'in_app' : 'notification_center', - ); - - const { order } = event.data; - - // Here in any case, we navigate to the order that was tapped, - // it should have been loaded via WebSocket already. - this.props.loadOrderAndNavigate(order); - } - - if (event && event.name === 'tasks:changed') { - tracker.logEvent( - analyticsEvent.courier._category, - analyticsEvent.courier.tasksChangedMessage, - message.foreground ? 'in_app' : 'notification_center', - ); - - if (message.foreground) { - this.props.pushNotification('tasks:changed', { - date: event.data.date, - }); - } else { - // user clicked on a notification in the notification center - this._onTasksChanged(event.data.date); - } - } - }, - onBackgroundMessage: message => { - const { event } = message.data; - if (event && event.name === 'order:created') { - this.props.loadOrder(event.data.order, order => { - if (order) { - // Simulate a WebSocket message - this.props.message({ - name: 'order:created', - data: { order }, - }); - } - }); - } - }, - }); - } - - componentWillUnmount() { - PushNotification.removeListeners(); - } - - _keyExtractor(item, index) { - switch (item.event) { - case 'order:created': - return `order:created:${item.params.order.id}`; - case 'tasks:changed': - return `tasks:changed:${moment()}`; + setTimeout(() => { + clear(); + }, 10000); } - } - - renderItem(notification) { - switch (notification.event) { - case 'order:created': - return this.renderOrderCreated(notification.params.order); - case 'tasks:changed': - return this.renderTasksChanged( - notification.params.date, - notification.params.added, - notification.params.removed, - ); - } - } - - _navigateToOrder(order) { - this._stopSound(); - this.props.clearNotifications(); - - NavigationHolder.dispatch( - CommonActions.navigate({ - name: 'RestaurantNav', - params: { - screen: 'Main', - params: { - restaurant: order.restaurant, - // We don't want to load orders again when navigating - loadOrders: false, - screen: 'RestaurantOrder', - params: { - order, - }, - }, - }, - }), - ); - } - - _navigateToTasks(date) { - this._stopSound(); - this.props.clearNotifications(); - - NavigationHolder.dispatch( - CommonActions.navigate({ - name: 'CourierNav', - params: { - screen: 'CourierHome', - params: { - screen: 'CourierTaskList', - }, - }, - }), - ); - - this.props.loadTasks(moment(date)); - } - - renderOrderCreated(order) { - return ( - this._navigateToOrder(order)}> - {this.props.t('NOTIFICATION_ORDER_CREATED_TITLE')} - - - ); - } - - renderTasksChanged(date, added, removed) { - return ( - this._navigateToTasks(date)}> - - - {this.props.t('NOTIFICATION_TASKS_CHANGED_TITLE')} - - {added && Array.isArray(added) && added.length > 0 && ( - - {this.props.t('NOTIFICATION_TASKS_ADDED', { - count: added.length, - })} - - )} - {removed && Array.isArray(removed) && removed.length > 0 && ( - - {this.props.t('NOTIFICATION_TASKS_REMOVED', { - count: removed.length, - })} - - )} - - - - ); - } - - renderModalContent() { - return ( - - - - - - {this.props.t('NEW_NOTIFICATION')} - - - - this.renderItem(item)} - /> - this.props.clearNotifications()}> - {this.props.t('CLOSE')} - - - ); - } - - render() { - return ( - - {this.renderModalContent()} - - ); - } + }, [isSoundPlaying, clear]); + + return ( + { + clear(); + }} + /> + ); } - -const styles = StyleSheet.create({ - heading: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 25, - backgroundColor: '#39CCCC', - }, - footer: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 25, - }, - item: { - paddingVertical: 25, - paddingHorizontal: 20, - borderBottomColor: '#f7f7f7', - borderBottomWidth: StyleSheet.hairlineWidth, - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, -}); - -function mapStateToProps(state) { - return { - currentRoute: state.app.currentRoute, - isModalVisible: state.app.notifications.length > 0, - notifications: state.app.notifications, - tasksChangedAlertSound: selectTasksChangedAlertSound(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - loadOrder: (order, cb) => dispatch(loadOrder(order, cb)), - loadOrderAndNavigate: order => dispatch(loadOrderAndNavigate(order)), - loadOrderAndPushNotification: order => - dispatch(loadOrderAndPushNotification(order)), - loadTasks: date => dispatch(loadTasks(date)), - registerPushNotificationToken: token => - dispatch(registerPushNotificationToken(token)), - clearNotifications: () => dispatch(clearNotifications()), - pushNotification: (event, params) => - dispatch(pushNotification(event, params)), - message: payload => dispatch(wsMessage(payload)), - }; -} - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(withTranslation()(NotificationHandler)); diff --git a/src/components/NotificationModal.js b/src/components/NotificationModal.js new file mode 100644 index 000000000..36663bf1f --- /dev/null +++ b/src/components/NotificationModal.js @@ -0,0 +1,173 @@ +import Modal from 'react-native-modal'; +import ModalContent from './ModalContent'; +import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { Icon, Text } from 'native-base'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import moment from 'moment/moment'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import NavigationHolder from '../NavigationHolder'; +import { CommonActions } from '@react-navigation/native'; +import { useDispatch } from 'react-redux'; +import { loadTasks } from '../redux/Courier'; + +const styles = StyleSheet.create({ + heading: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 25, + backgroundColor: '#39CCCC', + }, + footer: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 25, + }, + item: { + paddingVertical: 25, + paddingHorizontal: 20, + borderBottomColor: '#f7f7f7', + borderBottomWidth: StyleSheet.hairlineWidth, + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, +}); + +export default function NotificationModal({ notifications, onDismiss }) { + const isModalVisible = notifications.length > 0; + + const { t } = useTranslation(); + + const dispatch = useDispatch(); + + const _keyExtractor = (item, index) => { + switch (item.event) { + case 'order:created': + return `order:created:${item.params.order.id}`; + case 'tasks:changed': + return `tasks:changed:${moment()}`; + } + }; + + const _navigateToOrder = order => { + onDismiss(); + + NavigationHolder.dispatch( + CommonActions.navigate({ + name: 'RestaurantNav', + params: { + screen: 'Main', + params: { + restaurant: order.restaurant, + // We don't want to load orders again when navigating + loadOrders: false, + screen: 'RestaurantOrder', + params: { + order, + }, + }, + }, + }), + ); + }; + + const _navigateToTasks = date => { + onDismiss(); + + NavigationHolder.dispatch( + CommonActions.navigate({ + name: 'CourierNav', + params: { + screen: 'CourierHome', + params: { + screen: 'CourierTaskList', + }, + }, + }), + ); + + dispatch(loadTasks(moment(date))); + }; + + const renderOrderCreated = order => { + return ( + _navigateToOrder(order)}> + {t('NOTIFICATION_ORDER_CREATED_TITLE')} + + + ); + }; + + const renderTasksChanged = (date, added, removed) => { + return ( + _navigateToTasks(date)}> + + + {t('NOTIFICATION_TASKS_CHANGED_TITLE')} + + {added && Array.isArray(added) && added.length > 0 && ( + + {t('NOTIFICATION_TASKS_ADDED', { + count: added.length, + })} + + )} + {removed && Array.isArray(removed) && removed.length > 0 && ( + + {t('NOTIFICATION_TASKS_REMOVED', { + count: removed.length, + })} + + )} + + + + ); + }; + + const renderItem = notification => { + switch (notification.event) { + case 'order:created': + return renderOrderCreated(notification.params.order); + case 'tasks:changed': + return renderTasksChanged( + notification.params.date, + notification.params.added, + notification.params.removed, + ); + } + }; + + return ( + + + + + + {t('NEW_NOTIFICATION')} + + + renderItem(item)} + /> + onDismiss()}> + {t('CLOSE')} + + + + ); +} diff --git a/src/hooks/usePlayNotificationSound.js b/src/hooks/usePlayNotificationSound.js new file mode 100644 index 000000000..610aa7bd6 --- /dev/null +++ b/src/hooks/usePlayNotificationSound.js @@ -0,0 +1,76 @@ +import { useSelector } from 'react-redux'; +import { useCallback, useEffect, useState } from 'react'; +import Sound from 'react-native-sound'; +import { selectTasksChangedAlertSound } from '../redux/Courier'; + +// Make sure sound will play even when device is in silent mode +Sound.setCategory('Playback'); + +const includesNotification = (notifications, predicate) => { + return notifications.findIndex(predicate) !== -1; +}; + +export default function usePlayNotificationSound(notifications) { + const tasksChangedAlertSound = useSelector(selectTasksChangedAlertSound); + + const [sound, setSound] = useState(null); + const [isSoundReady, setIsSoundReady] = useState(false); + const [isSoundPlaying, setIsSoundPlaying] = useState(false); + + // load sound + useEffect(() => { + const bell = new Sound( + 'misstickle__indian_bell_chime.wav', + Sound.MAIN_BUNDLE, + error => { + if (error) { + return; + } + + bell.setNumberOfLoops(-1); + + setSound(bell); + setIsSoundReady(true); + }, + ); + }, []); + + const startSound = useCallback(() => { + if (isSoundReady && !isSoundPlaying) { + sound.play(success => { + if (!success) { + sound.reset(); + } + }); + + setIsSoundPlaying(true); + } + }, [isSoundReady, isSoundPlaying, sound]); + + const stopSound = useCallback(() => { + if (isSoundReady && isSoundPlaying) { + sound.stop(() => {}); + setIsSoundPlaying(false); + } + }, [isSoundPlaying, isSoundReady, sound]); + + useEffect(() => { + if (notifications.length > 0) { + if ( + includesNotification(notifications, n => n.event === 'order:created') + ) { + startSound(); + } else if ( + includesNotification(notifications, n => n.event === 'tasks:changed') + ) { + if (tasksChangedAlertSound) { + startSound(); + } + } + } else { + stopSound(); + } + }, [notifications, startSound, stopSound, tasksChangedAlertSound]); + + return { isSoundPlaying, stopSound }; +} diff --git a/src/hooks/usePushNotification.js b/src/hooks/usePushNotification.js new file mode 100644 index 000000000..50f51ee81 --- /dev/null +++ b/src/hooks/usePushNotification.js @@ -0,0 +1,114 @@ +import { useEffect } from 'react'; +import PushNotification from '../notifications/index.ios'; +import tracker from '../analytics/Tracker'; +import analyticsEvent from '../analytics/Event'; +import { useDispatch, useSelector } from 'react-redux'; +import { + pushNotification, + registerPushNotificationToken, +} from '../redux/App/actions'; +import { loadOrder, loadOrderAndNavigate } from '../redux/Restaurant/actions'; +import NavigationHolder from '../NavigationHolder'; +import moment from 'moment/moment'; +import { loadTasks } from '../redux/Courier'; +import { message as wsMessage } from '../redux/middlewares/CentrifugoMiddleware/actions'; +import { selectCurrentRoute } from '../redux/App/selectors'; + +function useOnRegister() { + const dispatch = useDispatch(); + + return token => { + dispatch(registerPushNotificationToken(token)); + }; +} + +function useOnNotification() { + const currentRoute = useSelector(selectCurrentRoute); + const dispatch = useDispatch(); + + const _onTasksChanged = date => { + if (currentRoute !== 'CourierTaskList') { + NavigationHolder.navigate('CourierTaskList', {}); + } + + dispatch(loadTasks(moment(date))); + }; + + return message => { + const { event } = message.data; + + if (event && event.name === 'order:created') { + tracker.logEvent( + analyticsEvent.restaurant._category, + analyticsEvent.restaurant.orderCreatedMessage, + message.foreground ? 'in_app' : 'notification_center', + ); + + const { order } = event.data; + + // Here in any case, we navigate to the order that was tapped, + // it should have been loaded via WebSocket already. + dispatch(loadOrderAndNavigate(order)); + } + + if (event && event.name === 'tasks:changed') { + tracker.logEvent( + analyticsEvent.courier._category, + analyticsEvent.courier.tasksChangedMessage, + message.foreground ? 'in_app' : 'notification_center', + ); + + if (message.foreground) { + dispatch( + pushNotification('tasks:changed', { + date: event.data.date, + }), + ); + } else { + // user clicked on a notification in the notification center + _onTasksChanged(event.data.date); + } + } + }; +} + +function useOnBackgroundMessage() { + const dispatch = useDispatch(); + + return message => { + const { event } = message.data; + if (event && event.name === 'order:created') { + dispatch( + loadOrder(event.data.order, order => { + if (order) { + // Simulate a WebSocket message + dispatch( + wsMessage({ + name: 'order:created', + data: { order }, + }), + ); + } + }), + ); + } + }; +} + +export default function usePushNotification() { + const onRegister = useOnRegister(); + const onNotification = useOnNotification(); + const onBackgroundMessage = useOnBackgroundMessage(); + + useEffect(() => { + PushNotification.configure({ + onRegister: token => onRegister(token), + onNotification: message => onNotification(message), + onBackgroundMessage: message => onBackgroundMessage(message), + }); + + return () => { + PushNotification.removeListeners(); + }; + }, [onBackgroundMessage, onNotification, onRegister]); +} diff --git a/src/redux/App/selectors.js b/src/redux/App/selectors.js index 2b3483261..a2aa97a6d 100644 --- a/src/redux/App/selectors.js +++ b/src/redux/App/selectors.js @@ -1,31 +1,32 @@ -import _ from 'lodash' -import { createSelector } from 'reselect' +import _ from 'lodash'; +import { createSelector } from 'reselect'; -import { selectIsTasksLoading } from '../Courier/taskSelectors' -import { selectIsDispatchFetching } from '../Dispatch/selectors' +import { selectIsTasksLoading } from '../Courier/taskSelectors'; +import { selectIsDispatchFetching } from '../Dispatch/selectors'; -export const selectUser = state => state.app.user -export const selectHttpClient = state => state.app.httpClient +export const selectUser = state => state.app.user; +export const selectHttpClient = state => state.app.httpClient; -export const selectCustomBuild = state => state.app.customBuild +export const selectCustomBuild = state => state.app.customBuild; -export const selectResumeCheckoutAfterActivation = state => state.app.resumeCheckoutAfterActivation +export const selectResumeCheckoutAfterActivation = state => + state.app.resumeCheckoutAfterActivation; // a user with an account export const selectIsAuthenticated = createSelector( selectUser, - (user) => !!(user && user.isAuthenticated()) -) + user => !!(user && user.isAuthenticated()), +); export const selectIsGuest = createSelector( selectUser, - (user) => !!(user && user.isGuest()) -) + user => !!(user && user.isGuest()), +); export const selectHttpClientHasCredentials = createSelector( selectHttpClient, - (httpClient) => !!(httpClient && !!httpClient.getToken()) -) + httpClient => !!(httpClient && !!httpClient.getToken()), +); export const selectIsLoading = createSelector( state => state.app.loading, @@ -33,99 +34,118 @@ export const selectIsLoading = createSelector( selectIsDispatchFetching, state => state.restaurant.isFetching, state => state.checkout.isFetching, - (isAppLoading, isTasksLoading, isDispatchLoading, isRestaurantLoading, isCheckoutLoading) => { - - return isAppLoading - || isTasksLoading - || isDispatchLoading - || isRestaurantLoading - || isCheckoutLoading - || false - } -) - -export const selectIsCentrifugoConnected = state => state.app.isCentrifugoConnected + ( + isAppLoading, + isTasksLoading, + isDispatchLoading, + isRestaurantLoading, + isCheckoutLoading, + ) => { + return ( + isAppLoading || + isTasksLoading || + isDispatchLoading || + isRestaurantLoading || + isCheckoutLoading || + false + ); + }, +); + +export const selectIsCentrifugoConnected = state => + state.app.isCentrifugoConnected; export const selectInitialRouteName = createSelector( selectUser, state => state.restaurant.myRestaurants, (user, restaurants) => { - if (user && user.isAuthenticated()) { - if (user.hasRole('ROLE_COURIER')) { - return 'CourierNav' + return 'CourierNav'; } if (user.hasRole('ROLE_DISPATCHER') || user.hasRole('ROLE_ADMIN')) { - return 'DispatchNav' + return 'DispatchNav'; } if (user.hasRole('ROLE_RESTAURANT') || user.hasRole('ROLE_STORE')) { - if (restaurants.length > 0) { - return 'RestaurantNav' + return 'RestaurantNav'; } - return 'StoreNav' + return 'StoreNav'; } } - return 'CheckoutNav' - } -) + return 'CheckoutNav'; + }, +); export const selectShowRestaurantsDrawerItem = createSelector( selectUser, selectIsAuthenticated, state => state.restaurant.myRestaurants, (user, isAuthenticated, restaurants) => - isAuthenticated && (user.hasRole('ROLE_ADMIN') || user.hasRole('ROLE_RESTAURANT')) && restaurants.length > 0 -) + isAuthenticated && + (user.hasRole('ROLE_ADMIN') || user.hasRole('ROLE_RESTAURANT')) && + restaurants.length > 0, +); export const selectServersWithURL = createSelector( state => state.app.servers, - (servers) => { - const serversWithURL = _.filter(servers, server => server.hasOwnProperty('coopcycle_url')) + servers => { + const serversWithURL = _.filter(servers, server => + server.hasOwnProperty('coopcycle_url'), + ); - return serversWithURL.sort((a, b) => a.city < b.city ? -1 : 1) - } -) + return serversWithURL.sort((a, b) => (a.city < b.city ? -1 : 1)); + }, +); export const selectServersInSameCity = createSelector( selectServersWithURL, state => state.app.baseURL, (servers, baseURL) => { if (!baseURL) { - return [] + return []; } - const currentServer = _.find(servers, (server) => server.coopcycle_url === baseURL) + const currentServer = _.find( + servers, + server => server.coopcycle_url === baseURL, + ); if (!currentServer) { - return [] + return []; } - const serversInSameCity = _.filter(servers, (server) => { - return server.city === currentServer.city - }) + const serversInSameCity = _.filter(servers, server => { + return server.city === currentServer.city; + }); // order servers randomly to avoid always same server as the first option - return _.shuffle(serversInSameCity) - } -) + return _.shuffle(serversInSameCity); + }, +); export const selectServersWithoutRepeats = createSelector( selectServersWithURL, - (servers) => { + servers => { return servers.reduce((withoutRepeatsAcc, server) => { - const serverCityAlreadyExist = withoutRepeatsAcc.some((nonRepeatedServer) => nonRepeatedServer.city === server.city) - if (!serverCityAlreadyExist) { - withoutRepeatsAcc.push(server) - } - return withoutRepeatsAcc; - }, []) - } -) + const serverCityAlreadyExist = withoutRepeatsAcc.some( + nonRepeatedServer => nonRepeatedServer.city === server.city, + ); + if (!serverCityAlreadyExist) { + withoutRepeatsAcc.push(server); + } + return withoutRepeatsAcc; + }, []); + }, +); + +export const selectIsSpinnerDelayEnabled = state => + state.app.isSpinnerDelayEnabled ?? true; + +export const selectCurrentRoute = state => state.app.currentRoute; -export const selectIsSpinnerDelayEnabled = state => state.app.isSpinnerDelayEnabled ?? true +export const selectNotifications = state => state.app.notifications; From 407424364c37ea40a135be0795795d5346fa8f7f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:22:34 -0700 Subject: [PATCH 14/69] fixed: import --- src/hooks/usePushNotification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/usePushNotification.js b/src/hooks/usePushNotification.js index 50f51ee81..60ff6d442 100644 --- a/src/hooks/usePushNotification.js +++ b/src/hooks/usePushNotification.js @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import PushNotification from '../notifications/index.ios'; +import PushNotification from '../notifications'; import tracker from '../analytics/Tracker'; import analyticsEvent from '../analytics/Event'; import { useDispatch, useSelector } from 'react-redux'; From f78b385ef096c1afa3faa1e26c1ee1f5c28ee7ba Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:13:48 -0700 Subject: [PATCH 15/69] ui fix; left margin --- src/navigation/restaurant/components/OrderListItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navigation/restaurant/components/OrderListItem.js b/src/navigation/restaurant/components/OrderListItem.js index 2ab482db5..f2613d6ea 100644 --- a/src/navigation/restaurant/components/OrderListItem.js +++ b/src/navigation/restaurant/components/OrderListItem.js @@ -21,7 +21,7 @@ const styles = StyleSheet.create({ flex: 1, flexDirection: 'row', marginVertical: 6, - marginLeft: 24 + 16, + marginLeft: 24, marginRight: 24, borderColor: '#E3E3E3', borderWidth: 1, From 6ce56a870117518421914027b29a64bf58bcacef Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:31:30 -0700 Subject: [PATCH 16/69] added: event constants --- src/components/NotificationModal.js | 10 ++++++---- src/domain/Order.js | 4 ++++ src/domain/TaskCollection.js | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 src/domain/TaskCollection.js diff --git a/src/components/NotificationModal.js b/src/components/NotificationModal.js index 36663bf1f..42bf44dcc 100644 --- a/src/components/NotificationModal.js +++ b/src/components/NotificationModal.js @@ -11,6 +11,8 @@ import NavigationHolder from '../NavigationHolder'; import { CommonActions } from '@react-navigation/native'; import { useDispatch } from 'react-redux'; import { loadTasks } from '../redux/Courier'; +import { EVENT as EVENT_ORDER } from '../domain/Order'; +import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; const styles = StyleSheet.create({ heading: { @@ -46,9 +48,9 @@ export default function NotificationModal({ notifications, onDismiss }) { const _keyExtractor = (item, index) => { switch (item.event) { - case 'order:created': + case EVENT_ORDER.CREATED: return `order:created:${item.params.order.id}`; - case 'tasks:changed': + case EVENT_TASK_COLLECTION.CHANGED: return `tasks:changed:${moment()}`; } }; @@ -135,9 +137,9 @@ export default function NotificationModal({ notifications, onDismiss }) { const renderItem = notification => { switch (notification.event) { - case 'order:created': + case EVENT_ORDER.CREATED: return renderOrderCreated(notification.params.order); - case 'tasks:changed': + case EVENT_TASK_COLLECTION.CHANGED: return renderTasksChanged( notification.params.date, notification.params.added, diff --git a/src/domain/Order.js b/src/domain/Order.js index c3da32370..0239468b9 100644 --- a/src/domain/Order.js +++ b/src/domain/Order.js @@ -7,3 +7,7 @@ export const STATE = { FULFILLED: 'fulfilled', CANCELLED: 'cancelled', }; + +export const EVENT = { + CREATED: 'order:created', +}; diff --git a/src/domain/TaskCollection.js b/src/domain/TaskCollection.js new file mode 100644 index 000000000..5de512f2a --- /dev/null +++ b/src/domain/TaskCollection.js @@ -0,0 +1,3 @@ +export const EVENT = { + CHANGED: 'tasks:changed', +}; From 21cf5440f6d428f12be81677d6992a1d9c669a7e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:57:52 -0700 Subject: [PATCH 17/69] changed: remove pop-up when a new order is autoaccepted (restaurant.autoAcceptOrdersEnabled), but the "ding" sound does remain --- src/components/NotificationHandler.js | 42 ++++++++++++- src/hooks/usePlayNotificationSound.js | 22 +------ src/hooks/usePushNotification.js | 34 +++++++---- src/redux/App/actions.js | 5 +- src/redux/App/reducers.js | 40 +++++++++++-- src/redux/Courier/taskMiddlewares.js | 4 +- .../Restaurant/__tests__/middlewares.test.js | 10 ++-- src/redux/Restaurant/actions.js | 18 ------ src/redux/Restaurant/middlewares.js | 60 ++++++++++--------- src/redux/store.js | 4 +- 10 files changed, 145 insertions(+), 94 deletions(-) diff --git a/src/components/NotificationHandler.js b/src/components/NotificationHandler.js index 44dba0fb1..4e9421e48 100644 --- a/src/components/NotificationHandler.js +++ b/src/components/NotificationHandler.js @@ -6,6 +6,10 @@ import NotificationModal from './NotificationModal'; import usePushNotification from '../hooks/usePushNotification'; import usePlayNotificationSound from '../hooks/usePlayNotificationSound'; import { selectNotifications } from '../redux/App/selectors'; +import { EVENT as EVENT_ORDER } from '../domain/Order'; +import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; +import { selectTasksChangedAlertSound } from '../redux/Courier'; +import { selectRestaurant } from '../redux/Restaurant/selectors'; /** * This component is used @@ -16,9 +20,41 @@ import { selectNotifications } from '../redux/App/selectors'; export default function NotificationHandler() { usePushNotification(); - const notifications = useSelector(selectNotifications); + const allNotifications = useSelector(selectNotifications); - const { isSoundPlaying, stopSound } = usePlayNotificationSound(notifications); + const tasksChangedAlertSound = useSelector(selectTasksChangedAlertSound); + + const notificationsWithSound = allNotifications.filter(notification => { + switch (notification.event) { + case EVENT_ORDER.CREATED: + return true; + case EVENT_TASK_COLLECTION.CHANGED: + return tasksChangedAlertSound; + default: + return false; + } + }); + + const restaurant = useSelector(selectRestaurant); + + const notificationsToDisplay = allNotifications.filter(notification => { + switch (notification.event) { + case EVENT_ORDER.CREATED: + if (restaurant && restaurant.autoAcceptOrdersEnabled) { + return false; + } else { + return true; + } + case EVENT_TASK_COLLECTION.CHANGED: + return true; + default: + return true; + } + }); + + const { isSoundPlaying, stopSound } = usePlayNotificationSound( + notificationsWithSound, + ); const dispatch = useDispatch(); @@ -38,7 +74,7 @@ export default function NotificationHandler() { return ( { clear(); }} diff --git a/src/hooks/usePlayNotificationSound.js b/src/hooks/usePlayNotificationSound.js index 610aa7bd6..08c4daa59 100644 --- a/src/hooks/usePlayNotificationSound.js +++ b/src/hooks/usePlayNotificationSound.js @@ -1,18 +1,10 @@ -import { useSelector } from 'react-redux'; import { useCallback, useEffect, useState } from 'react'; import Sound from 'react-native-sound'; -import { selectTasksChangedAlertSound } from '../redux/Courier'; // Make sure sound will play even when device is in silent mode Sound.setCategory('Playback'); -const includesNotification = (notifications, predicate) => { - return notifications.findIndex(predicate) !== -1; -}; - export default function usePlayNotificationSound(notifications) { - const tasksChangedAlertSound = useSelector(selectTasksChangedAlertSound); - const [sound, setSound] = useState(null); const [isSoundReady, setIsSoundReady] = useState(false); const [isSoundPlaying, setIsSoundPlaying] = useState(false); @@ -56,21 +48,11 @@ export default function usePlayNotificationSound(notifications) { useEffect(() => { if (notifications.length > 0) { - if ( - includesNotification(notifications, n => n.event === 'order:created') - ) { - startSound(); - } else if ( - includesNotification(notifications, n => n.event === 'tasks:changed') - ) { - if (tasksChangedAlertSound) { - startSound(); - } - } + startSound(); } else { stopSound(); } - }, [notifications, startSound, stopSound, tasksChangedAlertSound]); + }, [notifications, startSound, stopSound]); return { isSoundPlaying, stopSound }; } diff --git a/src/hooks/usePushNotification.js b/src/hooks/usePushNotification.js index 60ff6d442..03682804f 100644 --- a/src/hooks/usePushNotification.js +++ b/src/hooks/usePushNotification.js @@ -4,24 +4,31 @@ import tracker from '../analytics/Tracker'; import analyticsEvent from '../analytics/Event'; import { useDispatch, useSelector } from 'react-redux'; import { - pushNotification, + addNotification, registerPushNotificationToken, } from '../redux/App/actions'; import { loadOrder, loadOrderAndNavigate } from '../redux/Restaurant/actions'; import NavigationHolder from '../NavigationHolder'; import moment from 'moment/moment'; import { loadTasks } from '../redux/Courier'; -import { message as wsMessage } from '../redux/middlewares/CentrifugoMiddleware/actions'; import { selectCurrentRoute } from '../redux/App/selectors'; +import { EVENT as EVENT_ORDER } from '../domain/Order'; +import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; function useOnRegister() { const dispatch = useDispatch(); return token => { + console.log('useOnRegister token:', token); dispatch(registerPushNotificationToken(token)); }; } +/** + * called when a user taps on a notification in the notification center + * android: only called when the app is in the background + * ios: called when the app is in the foreground or background (?) + */ function useOnNotification() { const currentRoute = useSelector(selectCurrentRoute); const dispatch = useDispatch(); @@ -35,9 +42,11 @@ function useOnNotification() { }; return message => { + console.log('useOnNotification message:', message); + const { event } = message.data; - if (event && event.name === 'order:created') { + if (event && event.name === EVENT_ORDER.CREATED) { tracker.logEvent( analyticsEvent.restaurant._category, analyticsEvent.restaurant.orderCreatedMessage, @@ -51,7 +60,7 @@ function useOnNotification() { dispatch(loadOrderAndNavigate(order)); } - if (event && event.name === 'tasks:changed') { + if (event && event.name === EVENT_TASK_COLLECTION.CHANGED) { tracker.logEvent( analyticsEvent.courier._category, analyticsEvent.courier.tasksChangedMessage, @@ -60,7 +69,7 @@ function useOnNotification() { if (message.foreground) { dispatch( - pushNotification('tasks:changed', { + addNotification(event.name, { date: event.data.date, }), ); @@ -72,20 +81,25 @@ function useOnNotification() { }; } +/** + * called when a push notification is received while the app is in the foreground + * android only! + */ function useOnBackgroundMessage() { const dispatch = useDispatch(); return message => { + console.log('useOnBackgroundMessage message:', message.data); + const { event } = message.data; - if (event && event.name === 'order:created') { + + if (event && event.name === EVENT_ORDER.CREATED) { dispatch( loadOrder(event.data.order, order => { if (order) { - // Simulate a WebSocket message dispatch( - wsMessage({ - name: 'order:created', - data: { order }, + addNotification(event.name, { + order: order, }), ); } diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js index 333486386..49cb58bb4 100644 --- a/src/redux/App/actions.js +++ b/src/redux/App/actions.js @@ -39,7 +39,7 @@ export const DELETE_PUSH_NOTIFICATION_TOKEN_SUCCESS = '@app/DELETE_PUSH_NOTIFICA export const LOGIN = '@app/LOGIN' export const SET_LOADING = '@app/SET_LOADING' -export const PUSH_NOTIFICATION = '@app/PUSH_NOTIFICATION' +export const ADD_NOTIFICATION = '@app/ADD_NOTIFICATION' export const CLEAR_NOTIFICATIONS = '@app/CLEAR_NOTIFICATIONS' export const AUTHENTICATION_REQUEST = '@app/AUTHENTICATION_REQUEST' export const AUTHENTICATION_SUCCESS = '@app/AUTHENTICATION_SUCCESS' @@ -94,7 +94,8 @@ export const SET_SPINNER_DELAY_ENABLED = '@app/SET_IS_SPINNER_DELAY_ENABLED' */ export const setLoading = createAction(SET_LOADING) -export const pushNotification = createAction(PUSH_NOTIFICATION, (event, params = {}) => ({ event, params })) + +export const addNotification = createAction(ADD_NOTIFICATION, (event, params = {}) => ({ event, params })) export const clearNotifications = createAction(CLEAR_NOTIFICATIONS) export const _authenticationRequest = createAction(AUTHENTICATION_REQUEST) diff --git a/src/redux/App/reducers.js b/src/redux/App/reducers.js index cc54f204b..453926a8d 100644 --- a/src/redux/App/reducers.js +++ b/src/redux/App/reducers.js @@ -24,7 +24,7 @@ import { LOGIN_BY_EMAIL_ERRORS, LOGOUT_SUCCESS, ONBOARDED, - PUSH_NOTIFICATION, + ADD_NOTIFICATION, REGISTER_PUSH_NOTIFICATION_TOKEN, REGISTRATION_ERRORS, RESET_MODAL, @@ -48,6 +48,8 @@ import { SET_USER, } from './actions' import Config from 'react-native-config'; +import { EVENT as EVENT_ORDER } from '../../domain/Order'; +import { EVENT as EVENT_TASK_COLLECTION } from '../../domain/TaskCollection'; const initialState = { customBuild: !!Config.DEFAULT_SERVER, @@ -140,11 +142,39 @@ export default (state = initialState, action = {}) => { isCentrifugoConnected: false, } - case PUSH_NOTIFICATION: - return { - ...state, - notifications: state.notifications.concat([action.payload]), + case ADD_NOTIFICATION: { + // can happen on android where we receive both notification+data and data only messages; + // plus centrifugo messages + const isAlreadyExist = + state.notifications.findIndex(notification => { + const isSameEvent = notification.event === action.payload.event; + + if (isSameEvent) { + switch (notification.event) { + case EVENT_ORDER.CREATED: + return ( + notification.params.order.id === + action.payload.params.order.id + ); + case EVENT_TASK_COLLECTION.CHANGED: + return false; + default: + return false; + } + } else { + return false; + } + }) !== -1; + + if (isAlreadyExist) { + return state; + } else { + return { + ...state, + notifications: state.notifications.concat([action.payload]), + } } + } case CLEAR_NOTIFICATIONS: return { diff --git a/src/redux/Courier/taskMiddlewares.js b/src/redux/Courier/taskMiddlewares.js index ef93bd6c2..a3f6be33c 100644 --- a/src/redux/Courier/taskMiddlewares.js +++ b/src/redux/Courier/taskMiddlewares.js @@ -1,7 +1,7 @@ import { AppState } from 'react-native' import _ from 'lodash' -import { LOGOUT_SUCCESS, pushNotification } from '../App/actions' +import { LOGOUT_SUCCESS, addNotification } from '../App/actions' import { LOAD_TASKS_SUCCESS } from './taskActions' import { selectTasks } from './taskSelectors' @@ -47,7 +47,7 @@ export const ringOnTaskListUpdated = ({ getState, dispatch }) => { _.differenceWith(prevTasks, tasks, (a, b) => a['@id'] === b['@id']) if (addedTasks.length > 0 || removedTasks.length > 0) { - dispatch(pushNotification('tasks:changed', { + dispatch(addNotification('tasks:changed', { date: state.date, added: addedTasks, removed: removedTasks, diff --git a/src/redux/Restaurant/__tests__/middlewares.test.js b/src/redux/Restaurant/__tests__/middlewares.test.js index 33d64bedd..6fdb879cb 100644 --- a/src/redux/Restaurant/__tests__/middlewares.test.js +++ b/src/redux/Restaurant/__tests__/middlewares.test.js @@ -1,6 +1,6 @@ import { applyMiddleware, combineReducers, createStore } from 'redux' import thunk from 'redux-thunk' -import { ringOnNewOrderCreated } from '../middlewares' +import { notifyOnNewOrderCreated } from '../middlewares' import { loadOrderSuccess, loadOrdersSuccess } from '../actions' import { message as wsMessage } from '../../middlewares/CentrifugoMiddleware/actions' import restaurantReducer from '../reducers' @@ -31,7 +31,7 @@ describe('ringOnNewOrderCreated', () => { restaurant: restaurantReducer, }) - const store = createStore(reducer, preloadedState, applyMiddleware(ringOnNewOrderCreated)) + const store = createStore(reducer, preloadedState, applyMiddleware(notifyOnNewOrderCreated)) store.dispatch(loadOrdersSuccess([ { '@id': '/api/orders/1', state: 'new' }, @@ -66,7 +66,7 @@ describe('ringOnNewOrderCreated', () => { restaurant: restaurantReducer, }) - const store = createStore(reducer, preloadedState, applyMiddleware(ringOnNewOrderCreated)) + const store = createStore(reducer, preloadedState, applyMiddleware(notifyOnNewOrderCreated)) store.dispatch(loadOrderSuccess( { '@id': '/api/orders/1', state: 'new' } @@ -108,7 +108,7 @@ describe('ringOnNewOrderCreated', () => { restaurant: restaurantReducer, }) - const store = createStore(reducer, preloadedState, applyMiddleware(thunk, ringOnNewOrderCreated)) + const store = createStore(reducer, preloadedState, applyMiddleware(thunk, notifyOnNewOrderCreated)) store.dispatch(wsMessage( { @@ -154,7 +154,7 @@ describe('ringOnNewOrderCreated', () => { restaurant: restaurantReducer, }) - const store = createStore(reducer, preloadedState, applyMiddleware(ringOnNewOrderCreated)) + const store = createStore(reducer, preloadedState, applyMiddleware(notifyOnNewOrderCreated)) store.dispatch(loadOrderSuccess( { '@id': '/api/orders/1', state: 'new' } diff --git a/src/redux/Restaurant/actions.js b/src/redux/Restaurant/actions.js index c4e0aaa1b..87cadc7e2 100644 --- a/src/redux/Restaurant/actions.js +++ b/src/redux/Restaurant/actions.js @@ -7,7 +7,6 @@ import { Buffer } from 'buffer'; import DropdownHolder from '../../DropdownHolder'; import NavigationHolder from '../../NavigationHolder'; -import { pushNotification } from '../App/actions'; import { encodeForPrinter } from '../../utils/order'; import * as SunmiPrinterLibrary from '@mitsuharu/react-native-sunmi-printer-library'; @@ -390,23 +389,6 @@ export function loadOrderAndNavigate(order, cb) { }; } -export function loadOrderAndPushNotification(order) { - return function (dispatch, getState) { - const { app } = getState(); - const { httpClient } = app; - - dispatch(loadOrderRequest()); - - return httpClient - .get(order) - .then(res => { - dispatch(loadOrderSuccess(res)); - dispatch(pushNotification('order:created', { order: res })); - }) - .catch(e => dispatch(loadOrderFailure(e))); - }; -} - export function acceptOrder(order, cb) { return function (dispatch, getState) { const { app } = getState(); diff --git a/src/redux/Restaurant/middlewares.js b/src/redux/Restaurant/middlewares.js index 85de9fda3..d303954d3 100644 --- a/src/redux/Restaurant/middlewares.js +++ b/src/redux/Restaurant/middlewares.js @@ -1,47 +1,53 @@ -import { AppState } from 'react-native' -import _ from 'lodash' +import { AppState } from 'react-native'; +import _ from 'lodash'; -import { pushNotification } from '../App/actions' -import { selectUser } from '../App/selectors' -import { LOAD_ORDERS_SUCCESS } from './actions' - -export const ringOnNewOrderCreated = ({ getState, dispatch }) => { - - return (next) => (action) => { +import { addNotification } from '../App/actions'; +import { selectUser } from '../App/selectors'; +import { EVENT as EVENT_ORDER } from '../../domain/Order'; +import { MESSAGE as CENTRIFUGO_MESSAGE } from '../middlewares/CentrifugoMiddleware' +export const notifyOnNewOrderCreated = ({ getState, dispatch }) => { + return next => action => { if (AppState.currentState !== 'active') { - return next(action) + return next(action); } - // Avoid ringing on first load - if (action.type === LOAD_ORDERS_SUCCESS) { - return next(action) + // Only create notification for updates received via Centrifugo + if (action.type !== CENTRIFUGO_MESSAGE) { + return next(action); } - const prevState = getState() - const result = next(action) - const state = getState() + const prevState = getState(); + const result = next(action); + const state = getState(); - const user = selectUser(state) + const user = selectUser(state); const shouldShowAlert = - user && user.isAuthenticated() && (user.hasRole('ROLE_ADMIN') || user.hasRole('ROLE_RESTAURANT')) + user && + user.isAuthenticated() && + (user.hasRole('ROLE_ADMIN') || user.hasRole('ROLE_RESTAURANT')); if (!shouldShowAlert) { - return result + return result; } if (state.restaurant.orders.length > 0) { - if (state.restaurant.orders.length !== prevState.restaurant.orders.length) { - const orders = - _.differenceWith(state.restaurant.orders, prevState.restaurant.orders, (a, b) => (a['@id'] + ':' + a.state) === (b['@id'] + ':' + b.state)) + if ( + state.restaurant.orders.length !== prevState.restaurant.orders.length + ) { + const orders = _.differenceWith( + state.restaurant.orders, + prevState.restaurant.orders, + (a, b) => a['@id'] + ':' + a.state === b['@id'] + ':' + b.state, + ); orders.forEach(o => { if (o.state === 'new') { - dispatch(pushNotification('order:created', { order: o })) + dispatch(addNotification(EVENT_ORDER.CREATED, { order: o })); } - }) + }); } } - return result - } -} + return result; + }; +}; diff --git a/src/redux/store.js b/src/redux/store.js index 83eb898c4..409b585df 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -13,7 +13,7 @@ import HttpMiddleware from './middlewares/HttpMiddleware' import NetInfoMiddleware from './middlewares/NetInfoMiddleware' import PushNotificationMiddleware from './middlewares/PushNotificationMiddleware' import SentryMiddleware from './middlewares/SentryMiddleware' -import { ringOnNewOrderCreated } from './Restaurant/middlewares' +import { notifyOnNewOrderCreated } from './Restaurant/middlewares' import { ringOnTaskListUpdated } from './Courier/taskMiddlewares' import CentrifugoMiddleware from './middlewares/CentrifugoMiddleware' import { filterExpiredCarts } from './Checkout/middlewares'; @@ -34,7 +34,7 @@ if (!Config.DEFAULT_SERVER) { middlewares.push(...[ GeolocationMiddleware, BluetoothMiddleware, - ringOnNewOrderCreated, + notifyOnNewOrderCreated, ringOnTaskListUpdated, ]) } From 238b8e2492140c8f45d4f2e0a4f90c4444562f87 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:49:33 -0700 Subject: [PATCH 18/69] fixed: handle tasks:changed push notification on android while the app is in the foreground --- src/hooks/usePushNotification.js | 37 +++++++++++++++++++--------- src/redux/App/reducers.js | 2 +- src/redux/Courier/taskMiddlewares.js | 3 ++- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/hooks/usePushNotification.js b/src/hooks/usePushNotification.js index 03682804f..0d311ea9d 100644 --- a/src/hooks/usePushNotification.js +++ b/src/hooks/usePushNotification.js @@ -93,18 +93,31 @@ function useOnBackgroundMessage() { const { event } = message.data; - if (event && event.name === EVENT_ORDER.CREATED) { - dispatch( - loadOrder(event.data.order, order => { - if (order) { - dispatch( - addNotification(event.name, { - order: order, - }), - ); - } - }), - ); + if (event) { + switch (event.name) { + case EVENT_ORDER.CREATED: + dispatch( + loadOrder(event.data.order, order => { + if (order) { + dispatch( + addNotification(event.name, { + order: order, + }), + ); + } + }), + ); + break; + case EVENT_TASK_COLLECTION.CHANGED: + dispatch( + addNotification(event.name, { + date: event.data.date, + }), + ); + break; + default: + break; + } } }; } diff --git a/src/redux/App/reducers.js b/src/redux/App/reducers.js index 453926a8d..99a06f0fe 100644 --- a/src/redux/App/reducers.js +++ b/src/redux/App/reducers.js @@ -157,7 +157,7 @@ export default (state = initialState, action = {}) => { action.payload.params.order.id ); case EVENT_TASK_COLLECTION.CHANGED: - return false; + return (notification.params.date === action.payload.params.date) default: return false; } diff --git a/src/redux/Courier/taskMiddlewares.js b/src/redux/Courier/taskMiddlewares.js index a3f6be33c..def0b6dc9 100644 --- a/src/redux/Courier/taskMiddlewares.js +++ b/src/redux/Courier/taskMiddlewares.js @@ -4,6 +4,7 @@ import _ from 'lodash' import { LOGOUT_SUCCESS, addNotification } from '../App/actions' import { LOAD_TASKS_SUCCESS } from './taskActions' import { selectTasks } from './taskSelectors' +import { EVENT as EVENT_TASK_COLLECTION } from '../../domain/TaskCollection'; export const ringOnTaskListUpdated = ({ getState, dispatch }) => { @@ -47,7 +48,7 @@ export const ringOnTaskListUpdated = ({ getState, dispatch }) => { _.differenceWith(prevTasks, tasks, (a, b) => a['@id'] === b['@id']) if (addedTasks.length > 0 || removedTasks.length > 0) { - dispatch(addNotification('tasks:changed', { + dispatch(addNotification(EVENT_TASK_COLLECTION.CHANGED, { date: state.date, added: addedTasks, removed: removedTasks, From f7a2dee91bbc75b3d3e3c6b1cf5f3a2b76ef4c96 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:05:36 -0700 Subject: [PATCH 19/69] fixed: wrong param --- src/redux/Courier/taskMiddlewares.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redux/Courier/taskMiddlewares.js b/src/redux/Courier/taskMiddlewares.js index def0b6dc9..f076f47d6 100644 --- a/src/redux/Courier/taskMiddlewares.js +++ b/src/redux/Courier/taskMiddlewares.js @@ -49,7 +49,7 @@ export const ringOnTaskListUpdated = ({ getState, dispatch }) => { if (addedTasks.length > 0 || removedTasks.length > 0) { dispatch(addNotification(EVENT_TASK_COLLECTION.CHANGED, { - date: state.date, + date: date, added: addedTasks, removed: removedTasks, })) From 123f91ebb4db95bb01e80322c502c48975211985 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:14:27 -0700 Subject: [PATCH 20/69] added: a separate redux action for foreground notifications --- src/hooks/usePushNotification.js | 10 +- src/redux/App/actions.js | 6 + src/redux/App/reducers.js | 93 ++++++---- src/redux/Courier/taskEntityReducer.js | 4 +- .../Restaurant/__tests__/middlewares.test.js | 175 ------------------ src/redux/Restaurant/middlewares.js | 53 ------ src/redux/Restaurant/reducers.js | 4 +- .../CentrifugoMiddleware/actions.js | 4 +- .../middlewares/CentrifugoMiddleware/index.js | 4 +- src/redux/store.js | 2 - 10 files changed, 80 insertions(+), 275 deletions(-) delete mode 100644 src/redux/Restaurant/__tests__/middlewares.test.js delete mode 100644 src/redux/Restaurant/middlewares.js diff --git a/src/hooks/usePushNotification.js b/src/hooks/usePushNotification.js index 0d311ea9d..32f51e699 100644 --- a/src/hooks/usePushNotification.js +++ b/src/hooks/usePushNotification.js @@ -4,9 +4,9 @@ import tracker from '../analytics/Tracker'; import analyticsEvent from '../analytics/Event'; import { useDispatch, useSelector } from 'react-redux'; import { - addNotification, + foregroundPushNotification, registerPushNotificationToken, -} from '../redux/App/actions'; +} from '../redux/App/actions' import { loadOrder, loadOrderAndNavigate } from '../redux/Restaurant/actions'; import NavigationHolder from '../NavigationHolder'; import moment from 'moment/moment'; @@ -69,7 +69,7 @@ function useOnNotification() { if (message.foreground) { dispatch( - addNotification(event.name, { + foregroundPushNotification(event.name, { date: event.data.date, }), ); @@ -100,7 +100,7 @@ function useOnBackgroundMessage() { loadOrder(event.data.order, order => { if (order) { dispatch( - addNotification(event.name, { + foregroundPushNotification(event.name, { order: order, }), ); @@ -110,7 +110,7 @@ function useOnBackgroundMessage() { break; case EVENT_TASK_COLLECTION.CHANGED: dispatch( - addNotification(event.name, { + foregroundPushNotification(event.name, { date: event.data.date, }), ); diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js index 49cb58bb4..be383fa4a 100644 --- a/src/redux/App/actions.js +++ b/src/redux/App/actions.js @@ -39,8 +39,12 @@ export const DELETE_PUSH_NOTIFICATION_TOKEN_SUCCESS = '@app/DELETE_PUSH_NOTIFICA export const LOGIN = '@app/LOGIN' export const SET_LOADING = '@app/SET_LOADING' + +export const FOREGROUND_PUSH_NOTIFICATION = '@app/FOREGROUND_PUSH_NOTIFICATION' + export const ADD_NOTIFICATION = '@app/ADD_NOTIFICATION' export const CLEAR_NOTIFICATIONS = '@app/CLEAR_NOTIFICATIONS' + export const AUTHENTICATION_REQUEST = '@app/AUTHENTICATION_REQUEST' export const AUTHENTICATION_SUCCESS = '@app/AUTHENTICATION_SUCCESS' export const AUTHENTICATION_FAILURE = '@app/AUTHENTICATION_FAILURE' @@ -95,6 +99,8 @@ export const SET_SPINNER_DELAY_ENABLED = '@app/SET_IS_SPINNER_DELAY_ENABLED' export const setLoading = createAction(SET_LOADING) +export const foregroundPushNotification = createAction(FOREGROUND_PUSH_NOTIFICATION, (event, params = {}) => ({ event, params })) + export const addNotification = createAction(ADD_NOTIFICATION, (event, params = {}) => ({ event, params })) export const clearNotifications = createAction(CLEAR_NOTIFICATIONS) diff --git a/src/redux/App/reducers.js b/src/redux/App/reducers.js index 99a06f0fe..a7a3bc472 100644 --- a/src/redux/App/reducers.js +++ b/src/redux/App/reducers.js @@ -2,10 +2,15 @@ /* * App reducer, dealing with non-domain specific state */ -import { CONNECTED, DISCONNECTED } from '../middlewares/CentrifugoMiddleware' +import { + CENTRIFUGO_MESSAGE, + CONNECTED, + DISCONNECTED, +} from '../middlewares/CentrifugoMiddleware' import { ACCEPT_PRIVACY_POLICY, ACCEPT_TERMS_AND_CONDITIONS, + ADD_NOTIFICATION, AUTHENTICATION_FAILURE, AUTHENTICATION_REQUEST, AUTHENTICATION_SUCCESS, @@ -15,6 +20,7 @@ import { CLEAR_SELECT_SERVER_ERROR, CLOSE_MODAL, DELETE_PUSH_NOTIFICATION_TOKEN_SUCCESS, + FOREGROUND_PUSH_NOTIFICATION, LOAD_PRIVACY_POLICY_FAILURE, LOAD_PRIVACY_POLICY_REQUEST, LOAD_PRIVACY_POLICY_SUCCESS, @@ -24,7 +30,6 @@ import { LOGIN_BY_EMAIL_ERRORS, LOGOUT_SUCCESS, ONBOARDED, - ADD_NOTIFICATION, REGISTER_PUSH_NOTIFICATION_TOKEN, REGISTRATION_ERRORS, RESET_MODAL, @@ -102,6 +107,39 @@ const initialState = { isSpinnerDelayEnabled: true, } +function updateNotifications(state, event, params) { + // we use multiple channels to receive notifications (centrifugo; push notifications), + // so the notification might be already in the store + const isAlreadyExist = + state.notifications.findIndex(notification => { + const isSameEvent = notification.event === event; + + if (isSameEvent) { + switch (notification.event) { + case EVENT_ORDER.CREATED: + return ( + notification.params.order.id === params.order.id + ); + case EVENT_TASK_COLLECTION.CHANGED: + return (notification.params.date === params.date) + default: + return false; + } + } else { + return false; + } + }) !== -1; + + if (isAlreadyExist) { + return state; + } else { + return { + ...state, + notifications: state.notifications.concat([{ event, params }]), + } + } +} + export default (state = initialState, action = {}) => { switch (action.type) { case SET_BASE_URL: @@ -142,40 +180,31 @@ export default (state = initialState, action = {}) => { isCentrifugoConnected: false, } - case ADD_NOTIFICATION: { - // can happen on android where we receive both notification+data and data only messages; - // plus centrifugo messages - const isAlreadyExist = - state.notifications.findIndex(notification => { - const isSameEvent = notification.event === action.payload.event; - - if (isSameEvent) { - switch (notification.event) { - case EVENT_ORDER.CREATED: - return ( - notification.params.order.id === - action.payload.params.order.id - ); - case EVENT_TASK_COLLECTION.CHANGED: - return (notification.params.date === action.payload.params.date) - default: - return false; - } - } else { - return false; - } - }) !== -1; + case CENTRIFUGO_MESSAGE: { + const { name, data } = action.payload - if (isAlreadyExist) { - return state; - } else { - return { - ...state, - notifications: state.notifications.concat([action.payload]), - } + switch (name) { + case EVENT_ORDER.CREATED: + return updateNotifications(state, name, data); + // case EVENT_TASK_COLLECTION.CHANGED: + // this event is currently handled by taskMiddlewares; maybe we should move it here + default: + return state } } + case FOREGROUND_PUSH_NOTIFICATION: { + const { event, params } = action.payload + + return updateNotifications(state, event, params); + } + + case ADD_NOTIFICATION: { + const { event, params } = action.payload + + return updateNotifications(state, event, params); + } + case CLEAR_NOTIFICATIONS: return { ...state, diff --git a/src/redux/Courier/taskEntityReducer.js b/src/redux/Courier/taskEntityReducer.js index 824ae3a63..2e660f228 100644 --- a/src/redux/Courier/taskEntityReducer.js +++ b/src/redux/Courier/taskEntityReducer.js @@ -17,7 +17,7 @@ import { LOGOUT_SUCCESS, SET_USER, } from '../App/actions' -import { MESSAGE } from '../middlewares/CentrifugoMiddleware' +import { CENTRIFUGO_MESSAGE } from '../middlewares/CentrifugoMiddleware' import _ from 'lodash' /* @@ -189,7 +189,7 @@ export const tasksEntityReducer = (state = tasksEntityInitialState, action = {}) } return state - case MESSAGE: + case CENTRIFUGO_MESSAGE: return processWsMsg(state, action) case ADD_SIGNATURE: diff --git a/src/redux/Restaurant/__tests__/middlewares.test.js b/src/redux/Restaurant/__tests__/middlewares.test.js deleted file mode 100644 index 6fdb879cb..000000000 --- a/src/redux/Restaurant/__tests__/middlewares.test.js +++ /dev/null @@ -1,175 +0,0 @@ -import { applyMiddleware, combineReducers, createStore } from 'redux' -import thunk from 'redux-thunk' -import { notifyOnNewOrderCreated } from '../middlewares' -import { loadOrderSuccess, loadOrdersSuccess } from '../actions' -import { message as wsMessage } from '../../middlewares/CentrifugoMiddleware/actions' -import restaurantReducer from '../reducers' -import appReducer from '../../App/reducers' -import AppUser from '../../../AppUser' - -describe('ringOnNewOrderCreated', () => { - - beforeEach(() => { - jest.mock('react-native/Libraries/AppState/AppState', () => ({ - currentState: 'active', - })) - }) - - it('does nothing with action type "LOAD_ORDERS_SUCCESS"', () => { - - const preloadedState = { - app: { - notifications: [], - }, - restaurant: { - orders: [], - }, - } - - const reducer = combineReducers({ - app: appReducer, - restaurant: restaurantReducer, - }) - - const store = createStore(reducer, preloadedState, applyMiddleware(notifyOnNewOrderCreated)) - - store.dispatch(loadOrdersSuccess([ - { '@id': '/api/orders/1', state: 'new' }, - ])) - - const newState = store.getState() - - expect(newState).toMatchObject({ - app: { - notifications: [], - }, - restaurant: { - orders: [{ '@id': '/api/orders/1', state: 'new' }], - }, - }) - }) - - it('pushes new notification with action type "LOAD_ORDER_SUCCESS"', () => { - - const preloadedState = { - app: { - notifications: [], - user: new AppUser('bob', 'bob@example.com', 'abc123456', ['ROLE_RESTAURANT'], ''), - }, - restaurant: { - orders: [], - }, - } - - const reducer = combineReducers({ - app: appReducer, - restaurant: restaurantReducer, - }) - - const store = createStore(reducer, preloadedState, applyMiddleware(notifyOnNewOrderCreated)) - - store.dispatch(loadOrderSuccess( - { '@id': '/api/orders/1', state: 'new' } - )) - - const newState = store.getState() - - expect(newState).toMatchObject({ - app: { - notifications: [ - { - event: 'order:created', - params: { - order: { '@id': '/api/orders/1', state: 'new' }, - }, - }, - ], - }, - restaurant: { - orders: [{ '@id': '/api/orders/1', state: 'new' }], - }, - }) - }) - - it('pushes new notification with action type "MESSAGE"', () => { - - const preloadedState = { - app: { - notifications: [], - user: new AppUser('bob', 'bob@example.com', 'abc123456', ['ROLE_RESTAURANT'], ''), - }, - restaurant: { - orders: [], - }, - } - - const reducer = combineReducers({ - app: appReducer, - restaurant: restaurantReducer, - }) - - const store = createStore(reducer, preloadedState, applyMiddleware(thunk, notifyOnNewOrderCreated)) - - store.dispatch(wsMessage( - { - name: 'order:created', - data: { order: { '@id': '/api/orders/1', state: 'new' } }, - } - )) - - const newState = store.getState() - - expect(newState).toMatchObject({ - app: { - notifications: [ - { - event: 'order:created', - params: { - order: { '@id': '/api/orders/1', state: 'new' }, - }, - }, - ], - }, - restaurant: { - orders: [{ '@id': '/api/orders/1', state: 'new' }], - }, - }) - }) - - it('does nothing when order is already loaded', () => { - - const preloadedState = { - app: { - notifications: [], - }, - restaurant: { - orders: [ - { '@id': '/api/orders/1', state: 'new' }, - ], - }, - } - - const reducer = combineReducers({ - app: appReducer, - restaurant: restaurantReducer, - }) - - const store = createStore(reducer, preloadedState, applyMiddleware(notifyOnNewOrderCreated)) - - store.dispatch(loadOrderSuccess( - { '@id': '/api/orders/1', state: 'new' } - )) - - const newState = store.getState() - - expect(newState).toMatchObject({ - app: { - notifications: [], - }, - restaurant: { - orders: [{ '@id': '/api/orders/1', state: 'new' }], - }, - }) - }) - -}) diff --git a/src/redux/Restaurant/middlewares.js b/src/redux/Restaurant/middlewares.js deleted file mode 100644 index d303954d3..000000000 --- a/src/redux/Restaurant/middlewares.js +++ /dev/null @@ -1,53 +0,0 @@ -import { AppState } from 'react-native'; -import _ from 'lodash'; - -import { addNotification } from '../App/actions'; -import { selectUser } from '../App/selectors'; -import { EVENT as EVENT_ORDER } from '../../domain/Order'; -import { MESSAGE as CENTRIFUGO_MESSAGE } from '../middlewares/CentrifugoMiddleware' - -export const notifyOnNewOrderCreated = ({ getState, dispatch }) => { - return next => action => { - if (AppState.currentState !== 'active') { - return next(action); - } - - // Only create notification for updates received via Centrifugo - if (action.type !== CENTRIFUGO_MESSAGE) { - return next(action); - } - - const prevState = getState(); - const result = next(action); - const state = getState(); - - const user = selectUser(state); - const shouldShowAlert = - user && - user.isAuthenticated() && - (user.hasRole('ROLE_ADMIN') || user.hasRole('ROLE_RESTAURANT')); - - if (!shouldShowAlert) { - return result; - } - - if (state.restaurant.orders.length > 0) { - if ( - state.restaurant.orders.length !== prevState.restaurant.orders.length - ) { - const orders = _.differenceWith( - state.restaurant.orders, - prevState.restaurant.orders, - (a, b) => a['@id'] + ':' + a.state === b['@id'] + ':' + b.state, - ); - orders.forEach(o => { - if (o.state === 'new') { - dispatch(addNotification(EVENT_ORDER.CREATED, { order: o })); - } - }); - } - } - - return result; - }; -}; diff --git a/src/redux/Restaurant/reducers.js b/src/redux/Restaurant/reducers.js index aa5bd1f5f..7ec23ba0b 100644 --- a/src/redux/Restaurant/reducers.js +++ b/src/redux/Restaurant/reducers.js @@ -68,7 +68,7 @@ import { LOAD_MY_RESTAURANTS_SUCCESS, } from '../App/actions'; -import { MESSAGE } from '../middlewares/CentrifugoMiddleware/actions'; +import { CENTRIFUGO_MESSAGE } from '../middlewares/CentrifugoMiddleware/actions'; import moment from 'moment'; import _ from 'lodash'; @@ -472,7 +472,7 @@ export default (state = initialState, action = {}) => { isSunmiPrinter: true, }; - case MESSAGE: + case CENTRIFUGO_MESSAGE: if (action.payload.name && action.payload.data) { const { name, data } = action.payload; diff --git a/src/redux/middlewares/CentrifugoMiddleware/actions.js b/src/redux/middlewares/CentrifugoMiddleware/actions.js index 3f67adc67..0b9049c51 100644 --- a/src/redux/middlewares/CentrifugoMiddleware/actions.js +++ b/src/redux/middlewares/CentrifugoMiddleware/actions.js @@ -3,7 +3,7 @@ import { createAction } from 'redux-actions' import { updateTask } from '../../Dispatch/actions' export const CONNECT = '@centrifugo/CONNECT' -export const MESSAGE = '@centrifugo/MESSAGE' +export const CENTRIFUGO_MESSAGE = '@centrifugo/MESSAGE' export const CONNECTED = '@centrifugo/CONNECTED' export const DISCONNECTED = '@centrifugo/DISCONNECTED' @@ -12,7 +12,7 @@ export const connect = createAction(CONNECT) export const connected = createAction(CONNECTED) export const disconnected = createAction(DISCONNECTED) -export const _message = createAction(MESSAGE) +export const _message = createAction(CENTRIFUGO_MESSAGE) export function message(payload) { diff --git a/src/redux/middlewares/CentrifugoMiddleware/index.js b/src/redux/middlewares/CentrifugoMiddleware/index.js index 1f581424b..cd15c7aad 100644 --- a/src/redux/middlewares/CentrifugoMiddleware/index.js +++ b/src/redux/middlewares/CentrifugoMiddleware/index.js @@ -2,10 +2,10 @@ import Centrifuge from 'centrifuge' import parseUrl from 'url-parse' import { + CENTRIFUGO_MESSAGE, CONNECT, CONNECTED, DISCONNECTED, - MESSAGE, connected, disconnected, message, @@ -81,7 +81,7 @@ export default ({ getState, dispatch }) => { } export { - MESSAGE, + CENTRIFUGO_MESSAGE, CONNECTED, DISCONNECTED, connected, diff --git a/src/redux/store.js b/src/redux/store.js index 409b585df..ace96a8a1 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -13,7 +13,6 @@ import HttpMiddleware from './middlewares/HttpMiddleware' import NetInfoMiddleware from './middlewares/NetInfoMiddleware' import PushNotificationMiddleware from './middlewares/PushNotificationMiddleware' import SentryMiddleware from './middlewares/SentryMiddleware' -import { notifyOnNewOrderCreated } from './Restaurant/middlewares' import { ringOnTaskListUpdated } from './Courier/taskMiddlewares' import CentrifugoMiddleware from './middlewares/CentrifugoMiddleware' import { filterExpiredCarts } from './Checkout/middlewares'; @@ -34,7 +33,6 @@ if (!Config.DEFAULT_SERVER) { middlewares.push(...[ GeolocationMiddleware, BluetoothMiddleware, - notifyOnNewOrderCreated, ringOnTaskListUpdated, ]) } From 7dc72db8b8337309838052c8a781a7c2f2daf8fb Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:41:44 -0700 Subject: [PATCH 21/69] improved debug tools; print redux store states and diff --- .env.dist | 1 + package.json | 1 + src/redux/middlewares/devSetup.js | 75 +++++++++++++++++++++++++++++++ src/redux/store.js | 11 ++--- yarn.lock | 5 +++ 5 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/redux/middlewares/devSetup.js diff --git a/.env.dist b/.env.dist index 4f2e514c9..1f2ec0812 100644 --- a/.env.dist +++ b/.env.dist @@ -11,3 +11,4 @@ GOOGLE_SIGN_IN_CLIENT_ID= DEFAULT_SERVER= PRIMARY_COLOR= SMARTLOOK_PROJECT_KEY= +DEBUG_REDUX_LOGGER_LEVEL=log diff --git a/package.json b/package.json index 9ca7d05bb..08cea6445 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "react-navigation-header-buttons": "^11.2.1", "react-query": "^3.39.3", "react-redux": "^7.2.8", + "recursive-diff": "^1.0.9", "reduce-reducers": "^1.0.4", "redux": "^4.2.1", "redux-actions": "^2.6.5", diff --git a/src/redux/middlewares/devSetup.js b/src/redux/middlewares/devSetup.js new file mode 100644 index 000000000..00babaacc --- /dev/null +++ b/src/redux/middlewares/devSetup.js @@ -0,0 +1,75 @@ +import { applyMiddleware } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import { createLogger } from 'redux-logger'; +import reactotron from 'reactotron-react-native'; +import rdiff from 'recursive-diff'; + +import Config from 'react-native-config'; + +import ReactotronConfig from '../../../ReactotronConfig'; + +function logger(storeAPI) { + return function wrapDispatch(next) { + return function handleAction(action) { + const stateBefore = storeAPI.getState(); + const result = next(action); + const stateAfter = storeAPI.getState(); + + const diff = rdiff.getDiff(stateBefore, stateAfter); + + const opToLabel = { + add: 'ADDED', + delete: 'DELETED', + update: 'UPDATED', + }; + + const unflattenDiff = diff.reduce((acc, { op, path, val }) => { + const lastKey = path.pop(); + const lastObj = path.reduce((acc, key) => { + if (!acc[key]) { + acc[key] = {}; + } + return acc[key]; + }, acc); + lastObj[`${lastKey} (${opToLabel[op]})`] = val; + return acc; + }, {}); + + if (diff.length > 0) { + console.log('REDUX STATE changed:', unflattenDiff); + } + + reactotron.display({ + name: 'REDUX STATE', + value: { + diff: unflattenDiff, + after: stateAfter, + before: stateBefore, + }, + preview: + diff.length > 0 ? `number of changes: ${diff.length}` : 'no changes', + important: false, + }); + + return result; + }; + }; +} + +export default function configureForDevelopment(middlewaresList) { + const middlewares = [...middlewaresList]; + + middlewares.push( + createLogger({ + level: Config.DEBUG_REDUX_LOGGER_LEVEL ?? 'log', + collapsed: true, + }), + ); + + middlewares.push(logger); + + return composeWithDevTools( + applyMiddleware(...middlewares), + ReactotronConfig.createEnhancer(), + ); +} diff --git a/src/redux/store.js b/src/redux/store.js index ace96a8a1..0833fc76a 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,11 +1,11 @@ import { applyMiddleware, createStore } from 'redux' import thunk from 'redux-thunk' import ReduxAsyncQueue from 'redux-async-queue' -import { composeWithDevTools } from 'redux-devtools-extension' -import { createLogger } from 'redux-logger' import { persistStore } from 'redux-persist' +import Config from 'react-native-config'; + import reducers from './reducers' import GeolocationMiddleware from './middlewares/GeolocationMiddleware' import BluetoothMiddleware from './middlewares/BluetoothMiddleware' @@ -16,7 +16,6 @@ import SentryMiddleware from './middlewares/SentryMiddleware' import { ringOnTaskListUpdated } from './Courier/taskMiddlewares' import CentrifugoMiddleware from './middlewares/CentrifugoMiddleware' import { filterExpiredCarts } from './Checkout/middlewares'; -import Config from 'react-native-config'; const middlewares = [ thunk, @@ -37,13 +36,9 @@ if (!Config.DEFAULT_SERVER) { ]) } -if (__DEV__) { - middlewares.push(createLogger({ collapsed: true })) -} - const middlewaresProxy = (middlewaresList) => { if (__DEV__) { - return composeWithDevTools(applyMiddleware(...middlewaresList), require('../../ReactotronConfig').default.createEnhancer()) + return require('./middlewares/devSetup').default(middlewaresList) } else { return applyMiddleware(...middlewaresList) } diff --git a/yarn.lock b/yarn.lock index d700b939d..6f00f7978 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13228,6 +13228,11 @@ recast@^0.21.0: source-map "~0.6.1" tslib "^2.0.1" +recursive-diff@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/recursive-diff/-/recursive-diff-1.0.9.tgz#e617cbfcf125d4d73954c06997289c2d3321d5f7" + integrity sha512-5mqpskzvXDo5Vy29Vj8tH30a0+XBmY11aqWGoN/uB94UHRwndX2EuPvH+WtbqOYkrwAF718/lDo6U4CB1qSSqQ== + recyclerlistview@^4.0.0: version "4.2.0" resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.0.tgz#a140149aaa470c9787a1426452651934240d69ef" From 86c2f7fc3789de8e51c3370220cd32a188e44d0f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:05:13 -0700 Subject: [PATCH 22/69] fixed: low volume alert is shown twice --- src/navigation/restaurant/Dashboard.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/navigation/restaurant/Dashboard.js b/src/navigation/restaurant/Dashboard.js index 5a236d7cb..ab31387ef 100644 --- a/src/navigation/restaurant/Dashboard.js +++ b/src/navigation/restaurant/Dashboard.js @@ -113,6 +113,7 @@ export default function DashboardPage({ navigation, route }) { const _checkSystemVolume = useCallback(() => { RNSound.getSystemVolume(volume => { if (volume < 0.4) { + setWasAlertShown(true); Alert.alert( t('RESTAURANT_SOUND_ALERT_TITLE'), t('RESTAURANT_SOUND_ALERT_MESSAGE'), @@ -120,8 +121,6 @@ export default function DashboardPage({ navigation, route }) { { text: t('RESTAURANT_SOUND_ALERT_CONFIRM'), onPress: () => { - setWasAlertShown(true); - // If would be cool to open the device settings directly, // but it is not (yet) possible to sent an Intent with extra flags // https://stackoverflow.com/questions/57514207/open-settings-using-linking-sendintent @@ -134,7 +133,6 @@ export default function DashboardPage({ navigation, route }) { { text: t('CANCEL'), style: 'cancel', - onPress: () => setWasAlertShown(true), }, ], ); From 176723006526423926509c84276848b1f56dcfbe Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:39:46 -0700 Subject: [PATCH 23/69] fixed: do not re-send the token if it does not change --- src/notifications/index.android.js | 1 - src/redux/App/reducers.js | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/notifications/index.android.js b/src/notifications/index.android.js index 50dc1218a..0d9c01ab0 100644 --- a/src/notifications/index.android.js +++ b/src/notifications/index.android.js @@ -73,7 +73,6 @@ class PushNotification { messaging() .getToken() .then(fcmToken => { - console.log(fcmToken) options.onRegister(fcmToken) }) } diff --git a/src/redux/App/reducers.js b/src/redux/App/reducers.js index a7a3bc472..6940a8af4 100644 --- a/src/redux/App/reducers.js +++ b/src/redux/App/reducers.js @@ -321,12 +321,19 @@ export default (state = initialState, action = {}) => { isInternetReachable: action.payload, } - case REGISTER_PUSH_NOTIFICATION_TOKEN: - return { - ...state, - pushNotificationToken: action.payload, - pushNotificationTokenSaved: false, + case REGISTER_PUSH_NOTIFICATION_TOKEN: { + const existingToken = state.pushNotificationToken + + if (existingToken === action.payload) { + return state + } else { + return { + ...state, + pushNotificationToken: action.payload, + pushNotificationTokenSaved: false, + } } + } case SAVE_PUSH_NOTIFICATION_TOKEN_SUCCESS: return { From bd51a570a4bdf81c665bc8ae794967c9805273d4 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:40:53 -0700 Subject: [PATCH 24/69] added: When these orders are accepted automatically it also automatically prints a ticket if a printer is connected --- src/components/NotificationHandler.js | 10 +- src/domain/Order.js | 2 + src/i18n/locales/en.json | 1 + src/navigation/restaurant/Dashboard.js | 2 + src/navigation/restaurant/Order.js | 9 +- .../restaurant/components/OrderList.js | 6 +- .../components/OrdersToPrintQueue.js | 118 +++++++++++++ src/redux/Restaurant/actions.js | 166 ++++++++++-------- src/redux/Restaurant/reducers.js | 63 ++++++- src/redux/Restaurant/selectors.js | 33 ++++ 10 files changed, 324 insertions(+), 86 deletions(-) create mode 100644 src/navigation/restaurant/components/OrdersToPrintQueue.js diff --git a/src/components/NotificationHandler.js b/src/components/NotificationHandler.js index 4e9421e48..c54b13cba 100644 --- a/src/components/NotificationHandler.js +++ b/src/components/NotificationHandler.js @@ -9,7 +9,7 @@ import { selectNotifications } from '../redux/App/selectors'; import { EVENT as EVENT_ORDER } from '../domain/Order'; import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; import { selectTasksChangedAlertSound } from '../redux/Courier'; -import { selectRestaurant } from '../redux/Restaurant/selectors'; +import { selectAutoAcceptOrdersEnabled } from '../redux/Restaurant/selectors'; /** * This component is used @@ -35,16 +35,12 @@ export default function NotificationHandler() { } }); - const restaurant = useSelector(selectRestaurant); + const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled); const notificationsToDisplay = allNotifications.filter(notification => { switch (notification.event) { case EVENT_ORDER.CREATED: - if (restaurant && restaurant.autoAcceptOrdersEnabled) { - return false; - } else { - return true; - } + return !autoAcceptOrdersEnabled; case EVENT_TASK_COLLECTION.CHANGED: return true; default: diff --git a/src/domain/Order.js b/src/domain/Order.js index 0239468b9..a2ff7fa5e 100644 --- a/src/domain/Order.js +++ b/src/domain/Order.js @@ -10,4 +10,6 @@ export const STATE = { export const EVENT = { CREATED: 'order:created', + ACCEPTED: 'order:accepted', + STATE_CHANGED: 'order:state_changed', }; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index d6aeda507..2e22a2722 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -322,6 +322,7 @@ "SCAN_FOR_PRINTERS": "Tap to scan", "SEARCH_WITH_ADDRESS": "Search « {{address}} »", "RESTAURANT_ORDER_CONNECT_PRINTER": "Connect a printer", + "RESTAURANT_ORDER_PRINTING": "Printing order {{number}} (#{{id}})", "SWIPE_TO_ACCEPT_REFUSE": "Slide to the right to accept, to the left to refuse", "ADD_COUPON": "Add a voucher code", "VOUCHER_CODE": "Voucher code", diff --git a/src/navigation/restaurant/Dashboard.js b/src/navigation/restaurant/Dashboard.js index ab31387ef..c065a0d7c 100644 --- a/src/navigation/restaurant/Dashboard.js +++ b/src/navigation/restaurant/Dashboard.js @@ -31,6 +31,7 @@ import { selectIsLoading, } from '../../redux/App/selectors'; import PushNotification from '../../notifications'; +import OrdersToPrintQueue from './components/OrdersToPrintQueue'; const RNSound = NativeModules.RNSound; @@ -177,6 +178,7 @@ export default function DashboardPage({ navigation, route }) { /> )} + navigate('RestaurantDate')} diff --git a/src/navigation/restaurant/Order.js b/src/navigation/restaurant/Order.js index 42c7a24a7..1fa584e8c 100644 --- a/src/navigation/restaurant/Order.js +++ b/src/navigation/restaurant/Order.js @@ -16,6 +16,10 @@ import { printOrder, } from '../../redux/Restaurant/actions'; import { isMultiVendor } from '../../utils/order'; +import { + selectIsPrinterConnected, + selectPrinter, +} from '../../redux/Restaurant/selectors' const OrderNotes = ({ order }) => { if (order.notes) { @@ -103,9 +107,8 @@ class OrderScreen extends Component { function mapStateToProps(state, ownProps) { return { order: ownProps.route.params?.order, - isPrinterConnected: - !!state.restaurant.printer || state.restaurant.isSunmiPrinter, - printer: state.restaurant.printer, + isPrinterConnected: selectIsPrinterConnected(state), + printer: selectPrinter(state), }; } diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index 7603a6c4b..ba4231bb3 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -4,12 +4,12 @@ import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { selectAcceptedOrders, + selectAutoAcceptOrdersEnabled, selectCancelledOrders, selectFulfilledOrders, selectNewOrders, selectPickedOrders, selectReadyOrders, - selectRestaurant, selectStartedOrders, } from '../../../redux/Restaurant/selectors'; import OrderListItem from './OrderListItem'; @@ -17,7 +17,7 @@ import OrderListSectionHeader from './OrderListSectionHeader'; import { View } from 'native-base'; export default function OrderList({ onItemClick }) { - const restaurant = useSelector(selectRestaurant); + const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled); const newOrders = useSelector(selectNewOrders); const acceptedOrders = useSelector(selectAcceptedOrders); @@ -30,7 +30,7 @@ export default function OrderList({ onItemClick }) { const { t } = useTranslation(); const sections = [ - ...(restaurant.autoAcceptOrdersEnabled + ...(autoAcceptOrdersEnabled ? [] : [ { diff --git a/src/navigation/restaurant/components/OrdersToPrintQueue.js b/src/navigation/restaurant/components/OrdersToPrintQueue.js new file mode 100644 index 000000000..81867b1bb --- /dev/null +++ b/src/navigation/restaurant/components/OrdersToPrintQueue.js @@ -0,0 +1,118 @@ +import React, { useEffect } from 'react'; +import { + ActivityIndicator, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native'; +import { Text } from 'native-base'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { + selectAutoAcceptOrdersEnabled, + selectIsPrinterConnected, + selectOrderById, + selectOrderIdsToPrint, + selectPrintingOrderId, +} from '../../../redux/Restaurant/selectors'; +import { printOrderById } from '../../../redux/Restaurant/actions'; +import { useNavigation } from '@react-navigation/native'; + +function usePrinter() { + const connected = useSelector(selectIsPrinterConnected); + + const orderIdsToPrint = useSelector(selectOrderIdsToPrint); + const printingOrderId = useSelector(selectPrintingOrderId); + + const dispatch = useDispatch(); + + useEffect(() => { + if (printingOrderId) { + return; + } + + if (orderIdsToPrint.length === 0) { + return; + } + + if (!connected) { + console.warn('Printer is not connected'); + return; + } + + const orderId = orderIdsToPrint[0]; + dispatch(printOrderById(orderId)); + }, [printingOrderId, orderIdsToPrint, connected, dispatch]); + + return { + printingOrderId, + printerConnected: connected, + }; +} + +export default function OrdersToPrintQueue() { + const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled); + + const { printerConnected, printingOrderId } = usePrinter(); + + const printingOrder = useSelector(state => + selectOrderById(state, printingOrderId), + ); + + const { t } = useTranslation(); + + const navigation = useNavigation(); + + if (autoAcceptOrdersEnabled && !printerConnected) { + return ( + { + navigation.navigate('RestaurantSettings', { + screen: 'RestaurantPrinter', + }); + }}> + {t('RESTAURANT_ORDER_CONNECT_PRINTER')} + + ); + } else if (printingOrder) { + return ( + + + {t('RESTAURANT_ORDER_PRINTING', { + number: printingOrder.number, + id: printingOrder.id, + })} + + + + ); + } else { + return null; + } +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 10, + paddingHorizontal: 20, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + disconnected: { + backgroundColor: '#f7b731', + borderBottomColor: '#eca309', + }, + printing: { + backgroundColor: '#26de81', + borderBottomColor: '#1cb568', + }, + text: { + color: 'white', + textAlign: 'center', + fontSize: 14, + fontWeight: '700', + }, +}); diff --git a/src/redux/Restaurant/actions.js b/src/redux/Restaurant/actions.js index 87cadc7e2..b23dd25bc 100644 --- a/src/redux/Restaurant/actions.js +++ b/src/redux/Restaurant/actions.js @@ -1,4 +1,5 @@ -import { createAction } from 'redux-actions'; +import { createAction, createAsyncThunk } from '@reduxjs/toolkit'; +import { createAction as createFsAction } from 'redux-actions'; import { CommonActions } from '@react-navigation/native'; import BleManager from 'react-native-ble-manager'; import _ from 'lodash'; @@ -17,8 +18,8 @@ import { LOAD_MY_RESTAURANTS_REQUEST, LOAD_MY_RESTAURANTS_SUCCESS, } from '../App/actions'; -import { createAsyncThunk } from '@reduxjs/toolkit'; import { selectHttpClient } from '../App/selectors'; +import { selectOrderById } from './selectors' /* * Action Types @@ -115,86 +116,88 @@ export const UPDATE_LOOPEAT_FORMATS_SUCCESS = * Action Creators */ -const loadMyRestaurantsRequest = createAction(LOAD_MY_RESTAURANTS_REQUEST); -const loadMyRestaurantsSuccess = createAction(LOAD_MY_RESTAURANTS_SUCCESS); -const loadMyRestaurantsFailure = createAction(LOAD_MY_RESTAURANTS_FAILURE); +const loadMyRestaurantsRequest = createFsAction(LOAD_MY_RESTAURANTS_REQUEST); +const loadMyRestaurantsSuccess = createFsAction(LOAD_MY_RESTAURANTS_SUCCESS); +const loadMyRestaurantsFailure = createFsAction(LOAD_MY_RESTAURANTS_FAILURE); -export const loadOrdersRequest = createAction(LOAD_ORDERS_REQUEST); -export const loadOrdersSuccess = createAction(LOAD_ORDERS_SUCCESS); -export const loadOrdersFailure = createAction(LOAD_ORDERS_FAILURE); +export const loadOrdersRequest = createFsAction(LOAD_ORDERS_REQUEST); +export const loadOrdersSuccess = createFsAction(LOAD_ORDERS_SUCCESS); +export const loadOrdersFailure = createFsAction(LOAD_ORDERS_FAILURE); -export const loadOrderRequest = createAction(LOAD_ORDER_REQUEST); -export const loadOrderSuccess = createAction(LOAD_ORDER_SUCCESS); -export const loadOrderFailure = createAction(LOAD_ORDER_FAILURE); +export const loadOrderRequest = createFsAction(LOAD_ORDER_REQUEST); +export const loadOrderSuccess = createFsAction(LOAD_ORDER_SUCCESS); +export const loadOrderFailure = createFsAction(LOAD_ORDER_FAILURE); -export const loadMenusRequest = createAction(LOAD_MENUS_REQUEST); -export const loadMenusSuccess = createAction(LOAD_MENUS_SUCCESS); -export const loadMenusFailure = createAction(LOAD_MENUS_FAILURE); -export const setCurrentMenu = createAction( +export const loadMenusRequest = createFsAction(LOAD_MENUS_REQUEST); +export const loadMenusSuccess = createFsAction(LOAD_MENUS_SUCCESS); +export const loadMenusFailure = createFsAction(LOAD_MENUS_FAILURE); +export const setCurrentMenu = createFsAction( SET_CURRENT_MENU, (restaurant, menu) => ({ restaurant, menu }), ); -export const acceptOrderRequest = createAction(ACCEPT_ORDER_REQUEST); -export const acceptOrderSuccess = createAction(ACCEPT_ORDER_SUCCESS); -export const acceptOrderFailure = createAction(ACCEPT_ORDER_FAILURE); +export const acceptOrderRequest = createFsAction(ACCEPT_ORDER_REQUEST); +export const acceptOrderSuccess = createFsAction(ACCEPT_ORDER_SUCCESS); +export const acceptOrderFailure = createFsAction(ACCEPT_ORDER_FAILURE); -export const refuseOrderRequest = createAction(REFUSE_ORDER_REQUEST); -export const refuseOrderSuccess = createAction(REFUSE_ORDER_SUCCESS); -export const refuseOrderFailure = createAction(REFUSE_ORDER_FAILURE); +export const refuseOrderRequest = createFsAction(REFUSE_ORDER_REQUEST); +export const refuseOrderSuccess = createFsAction(REFUSE_ORDER_SUCCESS); +export const refuseOrderFailure = createFsAction(REFUSE_ORDER_FAILURE); -export const delayOrderRequest = createAction(DELAY_ORDER_REQUEST); -export const delayOrderSuccess = createAction(DELAY_ORDER_SUCCESS); -export const delayOrderFailure = createAction(DELAY_ORDER_FAILURE); +export const delayOrderRequest = createFsAction(DELAY_ORDER_REQUEST); +export const delayOrderSuccess = createFsAction(DELAY_ORDER_SUCCESS); +export const delayOrderFailure = createFsAction(DELAY_ORDER_FAILURE); -export const fulfillOrderRequest = createAction(FULFILL_ORDER_REQUEST); -export const fulfillOrderSuccess = createAction(FULFILL_ORDER_SUCCESS); -export const fulfillOrderFailure = createAction(FULFILL_ORDER_FAILURE); +export const fulfillOrderRequest = createFsAction(FULFILL_ORDER_REQUEST); +export const fulfillOrderSuccess = createFsAction(FULFILL_ORDER_SUCCESS); +export const fulfillOrderFailure = createFsAction(FULFILL_ORDER_FAILURE); -export const cancelOrderRequest = createAction(CANCEL_ORDER_REQUEST); -export const cancelOrderSuccess = createAction(CANCEL_ORDER_SUCCESS); -export const cancelOrderFailure = createAction(CANCEL_ORDER_FAILURE); +export const cancelOrderRequest = createFsAction(CANCEL_ORDER_REQUEST); +export const cancelOrderSuccess = createFsAction(CANCEL_ORDER_SUCCESS); +export const cancelOrderFailure = createFsAction(CANCEL_ORDER_FAILURE); -export const changeStatusRequest = createAction(CHANGE_STATUS_REQUEST); -export const changeStatusSuccess = createAction(CHANGE_STATUS_SUCCESS); -export const changeStatusFailure = createAction(CHANGE_STATUS_FAILURE); +export const changeStatusRequest = createFsAction(CHANGE_STATUS_REQUEST); +export const changeStatusSuccess = createFsAction(CHANGE_STATUS_SUCCESS); +export const changeStatusFailure = createFsAction(CHANGE_STATUS_FAILURE); -export const changeRestaurant = createAction(CHANGE_RESTAURANT); -export const changeDate = createAction(CHANGE_DATE); +export const changeRestaurant = createFsAction(CHANGE_RESTAURANT); +export const changeDate = createFsAction(CHANGE_DATE); -export const loadProductsRequest = createAction(LOAD_PRODUCTS_REQUEST); -export const loadProductsSuccess = createAction(LOAD_PRODUCTS_SUCCESS); -export const loadProductsFailure = createAction(LOAD_PRODUCTS_FAILURE); +export const loadProductsRequest = createFsAction(LOAD_PRODUCTS_REQUEST); +export const loadProductsSuccess = createFsAction(LOAD_PRODUCTS_SUCCESS); +export const loadProductsFailure = createFsAction(LOAD_PRODUCTS_FAILURE); -export const loadProductOptionsSuccess = createAction( +export const loadProductOptionsSuccess = createFsAction( LOAD_PRODUCT_OPTIONS_SUCCESS, ); -export const setNextProductsPage = createAction(SET_NEXT_PRODUCTS_PAGE); -export const loadMoreProductsSuccess = createAction(LOAD_MORE_PRODUCTS_SUCCESS); -export const setHasMoreProducts = createAction(SET_HAS_MORE_PRODUCTS); +export const setNextProductsPage = createFsAction(SET_NEXT_PRODUCTS_PAGE); +export const loadMoreProductsSuccess = createFsAction( + LOAD_MORE_PRODUCTS_SUCCESS, +); +export const setHasMoreProducts = createFsAction(SET_HAS_MORE_PRODUCTS); -export const changeProductEnabledRequest = createAction( +export const changeProductEnabledRequest = createFsAction( CHANGE_PRODUCT_ENABLED_REQUEST, (product, enabled) => ({ product, enabled }), ); -export const changeProductEnabledSuccess = createAction( +export const changeProductEnabledSuccess = createFsAction( CHANGE_PRODUCT_ENABLED_SUCCESS, ); -export const changeProductEnabledFailure = createAction( +export const changeProductEnabledFailure = createFsAction( CHANGE_PRODUCT_ENABLED_FAILURE, (error, product, enabled) => ({ error, product, enabled }), ); -export const changeProductOptionValueEnabledRequest = createAction( +export const changeProductOptionValueEnabledRequest = createFsAction( CHANGE_PRODUCT_OPTION_VALUE_ENABLED_REQUEST, (productOptionValue, enabled) => ({ productOptionValue, enabled }), ); -export const changeProductOptionValueEnabledSuccess = createAction( +export const changeProductOptionValueEnabledSuccess = createFsAction( CHANGE_PRODUCT_OPTION_VALUE_ENABLED_SUCCESS, (productOptionValue, enabled) => ({ productOptionValue, enabled }), ); -export const changeProductOptionValueEnabledFailure = createAction( +export const changeProductOptionValueEnabledFailure = createFsAction( CHANGE_PRODUCT_OPTION_VALUE_ENABLED_FAILURE, (error, productOptionValue, enabled) => ({ error, @@ -203,39 +206,43 @@ export const changeProductOptionValueEnabledFailure = createAction( }), ); -export const closeRestaurantRequest = createAction(CLOSE_RESTAURANT_REQUEST); -export const closeRestaurantSuccess = createAction(CLOSE_RESTAURANT_SUCCESS); -export const closeRestaurantFailure = createAction(CLOSE_RESTAURANT_FAILURE); +export const closeRestaurantRequest = createFsAction(CLOSE_RESTAURANT_REQUEST); +export const closeRestaurantSuccess = createFsAction(CLOSE_RESTAURANT_SUCCESS); +export const closeRestaurantFailure = createFsAction(CLOSE_RESTAURANT_FAILURE); -export const deleteOpeningHoursSpecificationRequest = createAction( +export const deleteOpeningHoursSpecificationRequest = createFsAction( DELETE_OPENING_HOURS_SPECIFICATION_REQUEST, ); -export const deleteOpeningHoursSpecificationSuccess = createAction( +export const deleteOpeningHoursSpecificationSuccess = createFsAction( DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS, ); -export const deleteOpeningHoursSpecificationFailure = createAction( +export const deleteOpeningHoursSpecificationFailure = createFsAction( DELETE_OPENING_HOURS_SPECIFICATION_FAILURE, ); -export const printerConnected = createAction(PRINTER_CONNECTED); -export const printerDisconnected = createAction(PRINTER_DISCONNECTED); +export const printerConnected = createFsAction(PRINTER_CONNECTED); +export const printerDisconnected = createFsAction(PRINTER_DISCONNECTED); -export const bluetoothEnabled = createAction(BLUETOOTH_ENABLED); -export const bluetoothDisabled = createAction(BLUETOOTH_DISABLED); -const _bluetoothStartScan = createAction(BLUETOOTH_START_SCAN); -export const bluetoothStopScan = createAction(BLUETOOTH_STOP_SCAN); -export const bluetoothStarted = createAction(BLUETOOTH_STARTED); +export const bluetoothEnabled = createFsAction(BLUETOOTH_ENABLED); +export const bluetoothDisabled = createFsAction(BLUETOOTH_DISABLED); +const _bluetoothStartScan = createFsAction(BLUETOOTH_START_SCAN); +export const bluetoothStopScan = createFsAction(BLUETOOTH_STOP_SCAN); +export const bluetoothStarted = createFsAction(BLUETOOTH_STARTED); -export const sunmiPrinterDetected = createAction(SUNMI_PRINTER_DETECTED); +export const sunmiPrinterDetected = createFsAction(SUNMI_PRINTER_DETECTED); -export const setLoopeatFormats = createAction( +export const setLoopeatFormats = createFsAction( SET_LOOPEAT_FORMATS, (order, loopeatFormats) => ({ order, loopeatFormats }), ); -export const updateLoopeatFormatsSuccess = createAction( +export const updateLoopeatFormatsSuccess = createFsAction( UPDATE_LOOPEAT_FORMATS_SUCCESS, ); +export const printPending = createAction('PRINT_PENDING'); +export const printFulfilled = createAction('PRINT_FULFILLED'); +export const printRejected = createAction('PRINT_REJECTED'); + /* * Thunk Creators */ @@ -643,8 +650,23 @@ function bluetoothErrorToString(e) { : e; } +export function printOrderById(orderId) { + return async (dispatch, getState) => { + const order = selectOrderById(getState(), orderId); + + if (!order) { + console.warn('Order not found', orderId); + return; + } + + await dispatch(printOrder(order)) + }; +} + export function printOrder(order) { return async (dispatch, getState) => { + dispatch(printPending(order)); + const { printer, isSunmiPrinter } = getState().restaurant; try { @@ -653,13 +675,16 @@ export function printOrder(order) { await SunmiPrinterLibrary.sendRAWData( Buffer.from(encodeForPrinter(order, true)).toString('base64'), ); + dispatch(printFulfilled(order)); return; } } catch (e) { - console.log(e); + console.warn('printOrder with SunmiPrinter failed', e); } if (!printer) { + console.warn('No printer selected'); + dispatch(printRejected(order)); return; } @@ -675,6 +700,7 @@ export function printOrder(order) { await BleManager.connect(printer.id); } catch (e) { dispatch(printerDisconnected()); + dispatch(printRejected(order)); DropdownHolder.getDropdown().alertWithType( 'error', i18n.t('RESTAURANT_PRINTER_CONNECT_ERROR_TITLE'), @@ -748,14 +774,16 @@ export function printOrder(order) { writableCharacteristic.characteristic, Array.from(encoded), ); - - break; + dispatch(printFulfilled(order)); } catch (e) { - console.log('Write failed', e); + console.warn('printOrder | Write failed', e); + dispatch(printRejected(order)); } } } } catch (e) { + console.warn('printOrder | Error', e); + dispatch(printRejected(order)); DropdownHolder.getDropdown().alertWithType( 'error', i18n.t('RESTAURANT_PRINTER_CONNECT_ERROR_TITLE'), @@ -797,7 +825,7 @@ export function disconnectPrinter(device, cb) { // We use Promise.finally because if the state // contains a saved printer which is not connected anymore, // BleManager.disconnect will return an error - .catch(e => console.log(e)) + .catch(e => console.warn('disconnectPrinter', e)) .finally(() => { dispatch(printerDisconnected()); diff --git a/src/redux/Restaurant/reducers.js b/src/redux/Restaurant/reducers.js index 7ec23ba0b..5d24a71bf 100644 --- a/src/redux/Restaurant/reducers.js +++ b/src/redux/Restaurant/reducers.js @@ -1,3 +1,6 @@ +import moment from 'moment'; +import _ from 'lodash'; + import { ACCEPT_ORDER_FAILURE, ACCEPT_ORDER_REQUEST, @@ -59,6 +62,9 @@ import { SUNMI_PRINTER_DETECTED, UPDATE_LOOPEAT_FORMATS_SUCCESS, finishPreparing, + printFulfilled, + printPending, + printRejected, startPreparing, } from './actions'; @@ -70,8 +76,7 @@ import { import { CENTRIFUGO_MESSAGE } from '../middlewares/CentrifugoMiddleware/actions'; -import moment from 'moment'; -import _ from 'lodash'; +import { EVENT as EVENT_ORDER, STATE } from '../../domain/Order'; const initialState = { fetchError: null, // Error object describing the error @@ -92,6 +97,8 @@ const initialState = { isSunmiPrinter: false, bluetoothStarted: false, loopeatFormats: {}, + orderIdsToPrint: [], + printingOrderId: null, }; const spliceOrders = (state, payload) => { @@ -172,6 +179,21 @@ const spliceProductOptions = (state, payload) => { return state; }; +function updateOrdersToPrint(state, orderId) { + if (state.restaurant.autoAcceptOrdersEnabled) { + if (state.orderIdsToPrint.includes(orderId)) { + return state; + } else { + return { + ...state, + orderIdsToPrint: state.orderIdsToPrint.concat(orderId), + }; + } + } else { + return state; + } +} + export default (state = initialState, action = {}) => { let newState; @@ -477,13 +499,25 @@ export default (state = initialState, action = {}) => { const { name, data } = action.payload; switch (name) { - case 'order:created': + case EVENT_ORDER.CREATED: case 'order:picked': - case 'order:state_changed': return { ...state, orders: addOrReplace(state, data.order), }; + case EVENT_ORDER.STATE_CHANGED: { + const updatedOrdersState = { + ...state, + orders: addOrReplace(state, data.order), + }; + + if (data.order.state === STATE.ACCEPTED) { + return updateOrdersToPrint(updatedOrdersState, data.order['@id']); + } else { + return updatedOrdersState; + } + } + default: break; } @@ -491,6 +525,27 @@ export default (state = initialState, action = {}) => { return state; + case printPending.type: + return { + ...state, + printingOrderId: action.payload['@id'], + }; + + case printFulfilled.type: + return { + ...state, + orderIdsToPrint: state.orderIdsToPrint.filter( + orderId => orderId !== action.payload['@id'], + ), + printingOrderId: null, + }; + + case printRejected.type: + return { + ...state, + printingOrderId: null, + }; + case BLUETOOTH_STARTED: return { ...state, diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index f7e7ff21e..1fd6b3608 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -41,6 +41,11 @@ export const selectSpecialOpeningHoursSpecification = createSelector( }, ); +export const selectAutoAcceptOrdersEnabled = createSelector( + selectRestaurant, + restaurant => restaurant?.autoAcceptOrdersEnabled ?? false, +); + export const selectNewOrders = createSelector( selectDate, _selectOrders, @@ -118,3 +123,31 @@ export const selectFulfilledOrders = createSelector( (date, orders) => _.filter(orders, o => matchesDate(o, date) && o.state === STATE.FULFILLED), ); + +export const selectPrinter = state => state.restaurant.printer; + +const selectIsSunmiPrinter = state => state.restaurant.isSunmiPrinter; + +export const selectIsPrinterConnected = createSelector( + selectPrinter, + selectIsSunmiPrinter, + (printer, isSunmiPrinter) => Boolean(printer) || isSunmiPrinter, +); + +export const selectOrderIdsToPrint = state => state.restaurant.orderIdsToPrint; + +export const selectPrintingOrderId = state => state.restaurant.printingOrderId; + +const selectOrderId = (state, id) => id; + +export const selectOrderById = createSelector( + _selectOrders, + selectOrderId, + (orders, id) => { + if (id) { + return orders.find(order => order['@id'] === id); + } else { + return undefined; + } + }, +); From 4acab7c3e4f54be93c0ba8996b6e6ecf62bb5714 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:37:50 -0700 Subject: [PATCH 25/69] fixed: unnecessary usePushNotification updates --- src/hooks/usePushNotification.js | 191 ++++++++++++++----------------- src/redux/Courier/taskActions.js | 15 +++ 2 files changed, 102 insertions(+), 104 deletions(-) diff --git a/src/hooks/usePushNotification.js b/src/hooks/usePushNotification.js index 32f51e699..2390c2938 100644 --- a/src/hooks/usePushNotification.js +++ b/src/hooks/usePushNotification.js @@ -1,131 +1,114 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import PushNotification from '../notifications'; import tracker from '../analytics/Tracker'; import analyticsEvent from '../analytics/Event'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { foregroundPushNotification, registerPushNotificationToken, -} from '../redux/App/actions' +} from '../redux/App/actions'; import { loadOrder, loadOrderAndNavigate } from '../redux/Restaurant/actions'; -import NavigationHolder from '../NavigationHolder'; import moment from 'moment/moment'; -import { loadTasks } from '../redux/Courier'; -import { selectCurrentRoute } from '../redux/App/selectors'; import { EVENT as EVENT_ORDER } from '../domain/Order'; import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; +import { navigateAndLoadTasks } from '../redux/Courier/taskActions'; -function useOnRegister() { - const dispatch = useDispatch(); - - return token => { - console.log('useOnRegister token:', token); - dispatch(registerPushNotificationToken(token)); - }; -} - -/** - * called when a user taps on a notification in the notification center - * android: only called when the app is in the background - * ios: called when the app is in the foreground or background (?) - */ -function useOnNotification() { - const currentRoute = useSelector(selectCurrentRoute); +export default function usePushNotification() { const dispatch = useDispatch(); - const _onTasksChanged = date => { - if (currentRoute !== 'CourierTaskList') { - NavigationHolder.navigate('CourierTaskList', {}); - } - - dispatch(loadTasks(moment(date))); - }; - - return message => { - console.log('useOnNotification message:', message); - - const { event } = message.data; - - if (event && event.name === EVENT_ORDER.CREATED) { - tracker.logEvent( - analyticsEvent.restaurant._category, - analyticsEvent.restaurant.orderCreatedMessage, - message.foreground ? 'in_app' : 'notification_center', - ); - - const { order } = event.data; - - // Here in any case, we navigate to the order that was tapped, - // it should have been loaded via WebSocket already. - dispatch(loadOrderAndNavigate(order)); - } - - if (event && event.name === EVENT_TASK_COLLECTION.CHANGED) { - tracker.logEvent( - analyticsEvent.courier._category, - analyticsEvent.courier.tasksChangedMessage, - message.foreground ? 'in_app' : 'notification_center', - ); - - if (message.foreground) { - dispatch( - foregroundPushNotification(event.name, { - date: event.data.date, - }), + const onRegister = useCallback( + token => { + console.log('onRegister token:', token); + dispatch(registerPushNotificationToken(token)); + }, + [dispatch], + ); + + /** + * called when a user taps on a notification in the notification center + * android: notification is only shown when the app is in the background + * ios: notification is shown both in the foreground and background + */ + const onNotification = useCallback( + message => { + console.log('onNotification message:', message); + + const { event } = message.data; + + if (event && event.name === EVENT_ORDER.CREATED) { + tracker.logEvent( + analyticsEvent.restaurant._category, + analyticsEvent.restaurant.orderCreatedMessage, + message.foreground ? 'in_app' : 'notification_center', ); - } else { - // user clicked on a notification in the notification center - _onTasksChanged(event.data.date); - } - } - }; -} -/** - * called when a push notification is received while the app is in the foreground - * android only! - */ -function useOnBackgroundMessage() { - const dispatch = useDispatch(); + const { order } = event.data; - return message => { - console.log('useOnBackgroundMessage message:', message.data); + // Here in any case, we navigate to the order that was tapped, + // it should have been loaded via WebSocket already. + dispatch(loadOrderAndNavigate(order)); + } - const { event } = message.data; + if (event && event.name === EVENT_TASK_COLLECTION.CHANGED) { + tracker.logEvent( + analyticsEvent.courier._category, + analyticsEvent.courier.tasksChangedMessage, + message.foreground ? 'in_app' : 'notification_center', + ); - if (event) { - switch (event.name) { - case EVENT_ORDER.CREATED: - dispatch( - loadOrder(event.data.order, order => { - if (order) { - dispatch( - foregroundPushNotification(event.name, { - order: order, - }), - ); - } - }), - ); - break; - case EVENT_TASK_COLLECTION.CHANGED: + if (message.foreground) { dispatch( foregroundPushNotification(event.name, { date: event.data.date, }), ); - break; - default: - break; + } else { + dispatch(navigateAndLoadTasks(moment(event.data.date))); + } } - } - }; -} - -export default function usePushNotification() { - const onRegister = useOnRegister(); - const onNotification = useOnNotification(); - const onBackgroundMessage = useOnBackgroundMessage(); + }, + [dispatch], + ); + + /** + * called when a push notification is received while the app is in the foreground + * android only! + */ + const onBackgroundMessage = useCallback( + message => { + console.log('onBackgroundMessage message:', message.data); + + const { event } = message.data; + + if (event) { + switch (event.name) { + case EVENT_ORDER.CREATED: + dispatch( + loadOrder(event.data.order, order => { + if (order) { + dispatch( + foregroundPushNotification(event.name, { + order: order, + }), + ); + } + }), + ); + break; + case EVENT_TASK_COLLECTION.CHANGED: + dispatch( + foregroundPushNotification(event.name, { + date: event.data.date, + }), + ); + break; + default: + break; + } + } + }, + [dispatch], + ); useEffect(() => { PushNotification.configure({ diff --git a/src/redux/Courier/taskActions.js b/src/redux/Courier/taskActions.js index 4fa0c329e..924100021 100644 --- a/src/redux/Courier/taskActions.js +++ b/src/redux/Courier/taskActions.js @@ -9,6 +9,7 @@ import i18n from '../../i18n' import { selectPictures, selectSignatures } from './taskSelectors' import tracker from '../../analytics/Tracker' import analyticsEvent from '../../analytics/Event' +import { selectCurrentRoute } from '../App/selectors' /* * Action Types @@ -141,6 +142,20 @@ function showAlertAfterBulk(messages) { * Thunk Creators */ +export function navigateAndLoadTasks(selectedDate) { + + return function (dispatch, getState) { + const currentRoute = selectCurrentRoute(getState()) + if (currentRoute !== 'CourierTaskList') { + NavigationHolder.navigate('CourierTaskList', {}); + } + + NavigationHolder.navigate('Tasks', { selectedDate }) + + return dispatch(loadTasks(selectedDate)) + } +} + export function loadTasks(selectedDate, refresh = false) { return function (dispatch, getState) { From d283a03a695ee8bba250817ee9b68594bd64b2c7 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:16:03 -0700 Subject: [PATCH 26/69] fixed: disconnect centrifuge on logout --- src/redux/App/selectors.js | 4 +++- .../middlewares/CentrifugoMiddleware/index.js | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/redux/App/selectors.js b/src/redux/App/selectors.js index a2aa97a6d..04820bd7c 100644 --- a/src/redux/App/selectors.js +++ b/src/redux/App/selectors.js @@ -102,9 +102,11 @@ export const selectServersWithURL = createSelector( }, ); +export const selectBaseURL = state => state.app.baseURL; + export const selectServersInSameCity = createSelector( selectServersWithURL, - state => state.app.baseURL, + selectBaseURL, (servers, baseURL) => { if (!baseURL) { return []; diff --git a/src/redux/middlewares/CentrifugoMiddleware/index.js b/src/redux/middlewares/CentrifugoMiddleware/index.js index cd15c7aad..8af0dfe38 100644 --- a/src/redux/middlewares/CentrifugoMiddleware/index.js +++ b/src/redux/middlewares/CentrifugoMiddleware/index.js @@ -12,11 +12,13 @@ import { } from './actions' import { + selectBaseURL, selectHttpClient, selectHttpClientHasCredentials, selectIsAuthenticated, selectUser, } from '../../App/selectors' +import { LOGOUT_SUCCESS } from '../../App/actions' const isCentrifugoAction = ({ type }) => [ @@ -24,10 +26,20 @@ const isCentrifugoAction = ({ type }) => ].some((x) => x === type) export default ({ getState, dispatch }) => { + let centrifuge = null return (next) => (action) => { - // TODO Run if connected + if (action.type === LOGOUT_SUCCESS) { + const result = next(action) + + if (centrifuge && centrifuge.isConnected()) { + centrifuge.disconnect() + centrifuge = null + } + + return result + } if (!isCentrifugoAction(action)) { return next(action) @@ -40,7 +52,7 @@ export default ({ getState, dispatch }) => { } const httpClient = selectHttpClient(state) - const baseURL = state.app.baseURL + const baseURL = selectBaseURL(state) const user = selectUser(state) httpClient @@ -50,7 +62,7 @@ export default ({ getState, dispatch }) => { const url = parseUrl(baseURL) const protocol = url.protocol === 'https:' ? 'wss' : 'ws' - const centrifuge = new Centrifuge(`${protocol}://${url.hostname}/centrifugo/connection/websocket`, { + centrifuge = new Centrifuge(`${protocol}://${url.hostname}/centrifugo/connection/websocket`, { debug: __DEV__, onRefresh: function(ctx, cb) { httpClient From fe65010ed61596bb2da7d0615a3e646789d109b4 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:47:00 -0700 Subject: [PATCH 27/69] fixed: https://github.com/coopcycle/coopcycle-app/issues/929 --- src/domain/Order.js | 3 ++- src/redux/Restaurant/selectors.js | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/domain/Order.js b/src/domain/Order.js index a2ff7fa5e..401d4759c 100644 --- a/src/domain/Order.js +++ b/src/domain/Order.js @@ -9,7 +9,8 @@ export const STATE = { }; export const EVENT = { + STATE_CHANGED: 'order:state_changed', CREATED: 'order:created', ACCEPTED: 'order:accepted', - STATE_CHANGED: 'order:state_changed', + PICKED: 'order:picked', }; diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index 1fd6b3608..9d6fe771b 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -3,7 +3,7 @@ import { find } from 'lodash'; import moment from 'moment'; import _ from 'lodash'; import { matchesDate } from '../../utils/order'; -import { STATE } from '../../domain/Order'; +import { EVENT, STATE } from '../../domain/Order'; export const selectRestaurant = state => state.restaurant.restaurant; export const selectDate = state => state.restaurant.date; @@ -83,7 +83,10 @@ export const selectReadyOrders = createSelector( _.sortBy( _.filter( orders, - o => matchesDate(o, date) && o.state === STATE.READY && !o.assignedTo, + o => + matchesDate(o, date) && + o.state === STATE.READY && + o.events.findIndex(ev => ev.type === EVENT.PICKED) === -1, ), [o => moment.parseZone(o.pickupExpectedAt)], ), @@ -96,7 +99,9 @@ export const selectPickedOrders = createSelector( _.sortBy( _.filter( orders, - o => matchesDate(o, date) && o.state === STATE.READY && !!o.assignedTo, + o => + matchesDate(o, date) && + o.events.findIndex(ev => ev.type === EVENT.PICKED) !== -1, ), [o => moment.parseZone(o.pickupExpectedAt)], ), From 05a8dad3c415c8536e6325bb0c4fea11919c20c5 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 16 Apr 2024 10:27:28 -0700 Subject: [PATCH 28/69] (re) applied prettier rules --- src/navigation/restaurant/Order.js | 2 +- src/notifications/index.android.js | 21 +++++------ src/redux/App/actions.js | 18 ++++++---- src/redux/App/reducers.js | 26 +++++++------- src/redux/Courier/taskActions.js | 11 +++--- src/redux/Courier/taskEntityReducer.js | 2 +- src/redux/Courier/taskMiddlewares.js | 11 +++--- src/redux/Restaurant/actions.js | 33 +++++++++-------- .../CentrifugoMiddleware/actions.js | 10 +++--- .../middlewares/CentrifugoMiddleware/index.js | 35 ++++++++---------- src/redux/store.js | 36 +++++++++---------- 11 files changed, 103 insertions(+), 102 deletions(-) diff --git a/src/navigation/restaurant/Order.js b/src/navigation/restaurant/Order.js index 956339ddb..471dea151 100644 --- a/src/navigation/restaurant/Order.js +++ b/src/navigation/restaurant/Order.js @@ -19,7 +19,7 @@ import { isMultiVendor } from '../../utils/order'; import { selectIsPrinterConnected, selectPrinter, -} from '../../redux/Restaurant/selectors' +} from '../../redux/Restaurant/selectors'; const OrderNotes = ({ order }) => { if (order.notes) { diff --git a/src/notifications/index.android.js b/src/notifications/index.android.js index 0ed7dc948..5f8d353c5 100644 --- a/src/notifications/index.android.js +++ b/src/notifications/index.android.js @@ -60,16 +60,17 @@ class PushNotification { // On Android API level 32 and below, you do not need to request user permission. // This method can still be called on Android devices; however, and will always resolve successfully. For API level 33+ you will need to request the permission manually if (Platform.Version >= 33) { - PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS) - .then((results) => { - if (PermissionsAndroid.RESULTS.GRANTED === results) { - messaging() - .getToken() - .then(fcmToken => { - options.onRegister(fcmToken) - }) - } - }) + PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS, + ).then(results => { + if (PermissionsAndroid.RESULTS.GRANTED === results) { + messaging() + .getToken() + .then(fcmToken => { + options.onRegister(fcmToken); + }); + } + }); } else { messaging() .getToken() diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js index 220cffb1f..6b155df1d 100644 --- a/src/redux/App/actions.js +++ b/src/redux/App/actions.js @@ -46,8 +46,8 @@ export const SET_LOADING = '@app/SET_LOADING'; export const FOREGROUND_PUSH_NOTIFICATION = '@app/FOREGROUND_PUSH_NOTIFICATION'; -export const ADD_NOTIFICATION = '@app/ADD_NOTIFICATION' -export const CLEAR_NOTIFICATIONS = '@app/CLEAR_NOTIFICATIONS' +export const ADD_NOTIFICATION = '@app/ADD_NOTIFICATION'; +export const CLEAR_NOTIFICATIONS = '@app/CLEAR_NOTIFICATIONS'; export const AUTHENTICATION_REQUEST = '@app/AUTHENTICATION_REQUEST'; export const AUTHENTICATION_SUCCESS = '@app/AUTHENTICATION_SUCCESS'; @@ -112,12 +112,18 @@ export const SET_SPINNER_DELAY_ENABLED = '@app/SET_IS_SPINNER_DELAY_ENABLED'; * Action Creators */ -export const setLoading = createAction(SET_LOADING) +export const setLoading = createAction(SET_LOADING); -export const foregroundPushNotification = createAction(FOREGROUND_PUSH_NOTIFICATION, (event, params = {}) => ({ event, params })) +export const foregroundPushNotification = createAction( + FOREGROUND_PUSH_NOTIFICATION, + (event, params = {}) => ({ event, params }), +); -export const addNotification = createAction(ADD_NOTIFICATION, (event, params = {}) => ({ event, params })) -export const clearNotifications = createAction(CLEAR_NOTIFICATIONS) +export const addNotification = createAction( + ADD_NOTIFICATION, + (event, params = {}) => ({ event, params }), +); +export const clearNotifications = createAction(CLEAR_NOTIFICATIONS); export const _authenticationRequest = createAction(AUTHENTICATION_REQUEST); export const _authenticationSuccess = createAction(AUTHENTICATION_SUCCESS); diff --git a/src/redux/App/reducers.js b/src/redux/App/reducers.js index e5077c67f..33621a0a9 100644 --- a/src/redux/App/reducers.js +++ b/src/redux/App/reducers.js @@ -9,7 +9,7 @@ import { CENTRIFUGO_MESSAGE, CONNECTED, DISCONNECTED, -} from '../middlewares/CentrifugoMiddleware' +} from '../middlewares/CentrifugoMiddleware'; import { ACCEPT_PRIVACY_POLICY, @@ -55,7 +55,7 @@ import { SET_SETTINGS, SET_SPINNER_DELAY_ENABLED, SET_USER, -} from './actions' +} from './actions'; import { EVENT as EVENT_ORDER } from '../../domain/Order'; import { EVENT as EVENT_TASK_COLLECTION } from '../../domain/TaskCollection'; @@ -121,11 +121,9 @@ function updateNotifications(state, event, params) { if (isSameEvent) { switch (notification.event) { case EVENT_ORDER.CREATED: - return ( - notification.params.order.id === params.order.id - ); + return notification.params.order.id === params.order.id; case EVENT_TASK_COLLECTION.CHANGED: - return (notification.params.date === params.date) + return notification.params.date === params.date; default: return false; } @@ -140,7 +138,7 @@ function updateNotifications(state, event, params) { return { ...state, notifications: state.notifications.concat([{ event, params }]), - } + }; } } @@ -185,7 +183,7 @@ export default (state = initialState, action = {}) => { }; case CENTRIFUGO_MESSAGE: { - const { name, data } = action.payload + const { name, data } = action.payload; switch (name) { case EVENT_ORDER.CREATED: @@ -193,18 +191,18 @@ export default (state = initialState, action = {}) => { // case EVENT_TASK_COLLECTION.CHANGED: // this event is currently handled by taskMiddlewares; maybe we should move it here default: - return state + return state; } } case FOREGROUND_PUSH_NOTIFICATION: { - const { event, params } = action.payload + const { event, params } = action.payload; return updateNotifications(state, event, params); } case ADD_NOTIFICATION: { - const { event, params } = action.payload + const { event, params } = action.payload; return updateNotifications(state, event, params); } @@ -326,16 +324,16 @@ export default (state = initialState, action = {}) => { }; case REGISTER_PUSH_NOTIFICATION_TOKEN: { - const existingToken = state.pushNotificationToken + const existingToken = state.pushNotificationToken; if (existingToken === action.payload) { - return state + return state; } else { return { ...state, pushNotificationToken: action.payload, pushNotificationTokenSaved: false, - } + }; } } diff --git a/src/redux/Courier/taskActions.js b/src/redux/Courier/taskActions.js index 18ec8bc50..939b3c0cb 100644 --- a/src/redux/Courier/taskActions.js +++ b/src/redux/Courier/taskActions.js @@ -9,7 +9,7 @@ import analyticsEvent from '../../analytics/Event'; import tracker from '../../analytics/Tracker'; import i18n from '../../i18n'; import { selectPictures, selectSignatures } from './taskSelectors'; -import { selectCurrentRoute } from '../App/selectors' +import { selectCurrentRoute } from '../App/selectors'; /* * Action Types @@ -157,17 +157,16 @@ function showAlertAfterBulk(messages) { */ export function navigateAndLoadTasks(selectedDate) { - return function (dispatch, getState) { - const currentRoute = selectCurrentRoute(getState()) + const currentRoute = selectCurrentRoute(getState()); if (currentRoute !== 'CourierTaskList') { NavigationHolder.navigate('CourierTaskList', {}); } - NavigationHolder.navigate('Tasks', { selectedDate }) + NavigationHolder.navigate('Tasks', { selectedDate }); - return dispatch(loadTasks(selectedDate)) - } + return dispatch(loadTasks(selectedDate)); + }; } export function loadTasks(selectedDate, refresh = false) { diff --git a/src/redux/Courier/taskEntityReducer.js b/src/redux/Courier/taskEntityReducer.js index 076e4ad33..4ad2776e3 100644 --- a/src/redux/Courier/taskEntityReducer.js +++ b/src/redux/Courier/taskEntityReducer.js @@ -214,7 +214,7 @@ export const tasksEntityReducer = ( return state; case CENTRIFUGO_MESSAGE: - return processWsMsg(state, action) + return processWsMsg(state, action); case ADD_SIGNATURE: return { diff --git a/src/redux/Courier/taskMiddlewares.js b/src/redux/Courier/taskMiddlewares.js index 7edea6c1b..5ae1f2e87 100644 --- a/src/redux/Courier/taskMiddlewares.js +++ b/src/redux/Courier/taskMiddlewares.js @@ -1,9 +1,9 @@ import _ from 'lodash'; import { AppState } from 'react-native'; -import { LOGOUT_SUCCESS, addNotification } from '../App/actions' -import { LOAD_TASKS_SUCCESS } from './taskActions' -import { selectTasks } from './taskSelectors' +import { LOGOUT_SUCCESS, addNotification } from '../App/actions'; +import { LOAD_TASKS_SUCCESS } from './taskActions'; +import { selectTasks } from './taskSelectors'; import { EVENT as EVENT_TASK_COLLECTION } from '../../domain/TaskCollection'; export const ringOnTaskListUpdated = ({ getState, dispatch }) => { @@ -53,8 +53,9 @@ export const ringOnTaskListUpdated = ({ getState, dispatch }) => { (a, b) => a['@id'] === b['@id'], ); - if (addedTasks.length > 0 || removedTasks.length > 0) { - dispatch(addNotification(EVENT_TASK_COLLECTION.CHANGED, { + if (addedTasks.length > 0 || removedTasks.length > 0) { + dispatch( + addNotification(EVENT_TASK_COLLECTION.CHANGED, { date: date, added: addedTasks, removed: removedTasks, diff --git a/src/redux/Restaurant/actions.js b/src/redux/Restaurant/actions.js index c4be50a04..5c03d13a9 100644 --- a/src/redux/Restaurant/actions.js +++ b/src/redux/Restaurant/actions.js @@ -20,7 +20,7 @@ import { } from '../App/actions'; import { selectHttpClient } from '../App/selectors'; -import { selectOrderById } from './selectors' +import { selectOrderById } from './selectors'; /* * Action Types @@ -117,22 +117,25 @@ export const UPDATE_LOOPEAT_FORMATS_SUCCESS = * Action Creators */ -const loadMyRestaurantsRequest = createAction(LOAD_MY_RESTAURANTS_REQUEST) -const loadMyRestaurantsSuccess = createAction(LOAD_MY_RESTAURANTS_SUCCESS) -const loadMyRestaurantsFailure = createAction(LOAD_MY_RESTAURANTS_FAILURE) +const loadMyRestaurantsRequest = createAction(LOAD_MY_RESTAURANTS_REQUEST); +const loadMyRestaurantsSuccess = createAction(LOAD_MY_RESTAURANTS_SUCCESS); +const loadMyRestaurantsFailure = createAction(LOAD_MY_RESTAURANTS_FAILURE); -export const loadOrdersRequest = createAction(LOAD_ORDERS_REQUEST) -export const loadOrdersSuccess = createAction(LOAD_ORDERS_SUCCESS) -export const loadOrdersFailure = createAction(LOAD_ORDERS_FAILURE) +export const loadOrdersRequest = createAction(LOAD_ORDERS_REQUEST); +export const loadOrdersSuccess = createAction(LOAD_ORDERS_SUCCESS); +export const loadOrdersFailure = createAction(LOAD_ORDERS_FAILURE); -export const loadOrderRequest = createAction(LOAD_ORDER_REQUEST) -export const loadOrderSuccess = createAction(LOAD_ORDER_SUCCESS) -export const loadOrderFailure = createAction(LOAD_ORDER_FAILURE) +export const loadOrderRequest = createAction(LOAD_ORDER_REQUEST); +export const loadOrderSuccess = createAction(LOAD_ORDER_SUCCESS); +export const loadOrderFailure = createAction(LOAD_ORDER_FAILURE); -export const loadMenusRequest = createAction(LOAD_MENUS_REQUEST) -export const loadMenusSuccess = createAction(LOAD_MENUS_SUCCESS) -export const loadMenusFailure = createAction(LOAD_MENUS_FAILURE) -export const setCurrentMenu = createAction(SET_CURRENT_MENU, (restaurant, menu) => ({ restaurant, menu })) +export const loadMenusRequest = createAction(LOAD_MENUS_REQUEST); +export const loadMenusSuccess = createAction(LOAD_MENUS_SUCCESS); +export const loadMenusFailure = createAction(LOAD_MENUS_FAILURE); +export const setCurrentMenu = createAction( + SET_CURRENT_MENU, + (restaurant, menu) => ({ restaurant, menu }), +); export const acceptOrderRequest = createFsAction(ACCEPT_ORDER_REQUEST); export const acceptOrderSuccess = createFsAction(ACCEPT_ORDER_SUCCESS); @@ -657,7 +660,7 @@ export function printOrderById(orderId) { return; } - await dispatch(printOrder(order)) + await dispatch(printOrder(order)); }; } diff --git a/src/redux/middlewares/CentrifugoMiddleware/actions.js b/src/redux/middlewares/CentrifugoMiddleware/actions.js index 4b158aacc..eb2f55259 100644 --- a/src/redux/middlewares/CentrifugoMiddleware/actions.js +++ b/src/redux/middlewares/CentrifugoMiddleware/actions.js @@ -2,17 +2,17 @@ import { createAction } from 'redux-actions'; import { updateTask } from '../../Dispatch/actions'; -export const CONNECT = '@centrifugo/CONNECT' -export const CENTRIFUGO_MESSAGE = '@centrifugo/MESSAGE' -export const CONNECTED = '@centrifugo/CONNECTED' -export const DISCONNECTED = '@centrifugo/DISCONNECTED' +export const CONNECT = '@centrifugo/CONNECT'; +export const CENTRIFUGO_MESSAGE = '@centrifugo/MESSAGE'; +export const CONNECTED = '@centrifugo/CONNECTED'; +export const DISCONNECTED = '@centrifugo/DISCONNECTED'; export const connect = createAction(CONNECT); export const connected = createAction(CONNECTED); export const disconnected = createAction(DISCONNECTED); -export const _message = createAction(CENTRIFUGO_MESSAGE) +export const _message = createAction(CENTRIFUGO_MESSAGE); export function message(payload) { return function (dispatch, getState) { diff --git a/src/redux/middlewares/CentrifugoMiddleware/index.js b/src/redux/middlewares/CentrifugoMiddleware/index.js index 7e9c3283e..f02cd3a23 100644 --- a/src/redux/middlewares/CentrifugoMiddleware/index.js +++ b/src/redux/middlewares/CentrifugoMiddleware/index.js @@ -17,25 +17,24 @@ import { selectHttpClientHasCredentials, selectIsAuthenticated, selectUser, -} from '../../App/selectors' -import { LOGOUT_SUCCESS } from '../../App/actions' +} from '../../App/selectors'; +import { LOGOUT_SUCCESS } from '../../App/actions'; const isCentrifugoAction = ({ type }) => [CONNECT].some(x => x === type); export default ({ getState, dispatch }) => { - let centrifuge = null - - return (next) => (action) => { + let centrifuge = null; + return next => action => { if (action.type === LOGOUT_SUCCESS) { - const result = next(action) + const result = next(action); if (centrifuge && centrifuge.isConnected()) { - centrifuge.disconnect() - centrifuge = null + centrifuge.disconnect(); + centrifuge = null; } - return result + return result; } if (!isCentrifugoAction(action)) { @@ -51,15 +50,17 @@ export default ({ getState, dispatch }) => { return next(action); } - const httpClient = selectHttpClient(state) - const baseURL = selectBaseURL(state) - const user = selectUser(state) + const httpClient = selectHttpClient(state); + const baseURL = selectBaseURL(state); + const user = selectUser(state); httpClient.get('/api/centrifugo/token').then(tokenResponse => { const url = parseUrl(baseURL); const protocol = url.protocol === 'https:' ? 'wss' : 'ws'; - centrifuge = new Centrifuge(`${protocol}://${url.hostname}/centrifugo/connection/websocket`, { + centrifuge = new Centrifuge( + `${protocol}://${url.hostname}/centrifugo/connection/websocket`, + { debug: __DEV__, onRefresh: function (ctx, cb) { httpClient @@ -92,10 +93,4 @@ export default ({ getState, dispatch }) => { }; }; -export { - CENTRIFUGO_MESSAGE, - CONNECTED, - DISCONNECTED, - connected, - disconnected, -} +export { CENTRIFUGO_MESSAGE, CONNECTED, DISCONNECTED, connected, disconnected }; diff --git a/src/redux/store.js b/src/redux/store.js index 8b1a890aa..d1cdbddf2 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,20 +1,20 @@ -import { applyMiddleware, createStore } from 'redux' -import thunk from 'redux-thunk' -import ReduxAsyncQueue from 'redux-async-queue' +import { applyMiddleware, createStore } from 'redux'; +import thunk from 'redux-thunk'; +import ReduxAsyncQueue from 'redux-async-queue'; import { persistStore } from 'redux-persist'; import Config from 'react-native-config'; -import reducers from './reducers' -import GeolocationMiddleware from './middlewares/GeolocationMiddleware' -import BluetoothMiddleware from './middlewares/BluetoothMiddleware' -import HttpMiddleware from './middlewares/HttpMiddleware' -import NetInfoMiddleware from './middlewares/NetInfoMiddleware' -import PushNotificationMiddleware from './middlewares/PushNotificationMiddleware' -import SentryMiddleware from './middlewares/SentryMiddleware' -import { ringOnTaskListUpdated } from './Courier/taskMiddlewares' -import CentrifugoMiddleware from './middlewares/CentrifugoMiddleware' +import reducers from './reducers'; +import GeolocationMiddleware from './middlewares/GeolocationMiddleware'; +import BluetoothMiddleware from './middlewares/BluetoothMiddleware'; +import HttpMiddleware from './middlewares/HttpMiddleware'; +import NetInfoMiddleware from './middlewares/NetInfoMiddleware'; +import PushNotificationMiddleware from './middlewares/PushNotificationMiddleware'; +import SentryMiddleware from './middlewares/SentryMiddleware'; +import { ringOnTaskListUpdated } from './Courier/taskMiddlewares'; +import CentrifugoMiddleware from './middlewares/CentrifugoMiddleware'; import { filterExpiredCarts } from './Checkout/middlewares'; const middlewares = [ @@ -29,16 +29,14 @@ const middlewares = [ ]; if (!Config.DEFAULT_SERVER) { - middlewares.push(...[ - GeolocationMiddleware, - BluetoothMiddleware, - ringOnTaskListUpdated, - ]) + middlewares.push( + ...[GeolocationMiddleware, BluetoothMiddleware, ringOnTaskListUpdated], + ); } -const middlewaresProxy = (middlewaresList) => { +const middlewaresProxy = middlewaresList => { if (__DEV__) { - return require('./middlewares/devSetup').default(middlewaresList) + return require('./middlewares/devSetup').default(middlewaresList); } else { return applyMiddleware(...middlewaresList); } From 8f68f2c722317c88cb30fff1ccebd23e1c3ac8ed Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 16 Apr 2024 11:43:54 -0700 Subject: [PATCH 29/69] configure push notifications in redux middleware --- src/components/NotificationHandler.js | 8 +- src/hooks/usePushNotification.js | 124 ------------------ .../PushNotificationMiddleware/index.js | 101 ++++++++++++++ 3 files changed, 103 insertions(+), 130 deletions(-) delete mode 100644 src/hooks/usePushNotification.js diff --git a/src/components/NotificationHandler.js b/src/components/NotificationHandler.js index c54b13cba..06d114187 100644 --- a/src/components/NotificationHandler.js +++ b/src/components/NotificationHandler.js @@ -3,7 +3,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { clearNotifications } from '../redux/App/actions'; import NotificationModal from './NotificationModal'; -import usePushNotification from '../hooks/usePushNotification'; import usePlayNotificationSound from '../hooks/usePlayNotificationSound'; import { selectNotifications } from '../redux/App/selectors'; import { EVENT as EVENT_ORDER } from '../domain/Order'; @@ -13,13 +12,10 @@ import { selectAutoAcceptOrdersEnabled } from '../redux/Restaurant/selectors'; /** * This component is used - * 1/ To configure push notifications (see usePushNotification) - * 2/ To show notifications when the app is in foreground (see NotificationModal) - * 3/ To play a sound when a notification is received (see usePlayNotificationSound) + * 1/ To show notifications when the app is in foreground (see NotificationModal) + * 2/ To play a sound when a notification is received (see usePlayNotificationSound) */ export default function NotificationHandler() { - usePushNotification(); - const allNotifications = useSelector(selectNotifications); const tasksChangedAlertSound = useSelector(selectTasksChangedAlertSound); diff --git a/src/hooks/usePushNotification.js b/src/hooks/usePushNotification.js deleted file mode 100644 index 2390c2938..000000000 --- a/src/hooks/usePushNotification.js +++ /dev/null @@ -1,124 +0,0 @@ -import { useCallback, useEffect } from 'react'; -import PushNotification from '../notifications'; -import tracker from '../analytics/Tracker'; -import analyticsEvent from '../analytics/Event'; -import { useDispatch } from 'react-redux'; -import { - foregroundPushNotification, - registerPushNotificationToken, -} from '../redux/App/actions'; -import { loadOrder, loadOrderAndNavigate } from '../redux/Restaurant/actions'; -import moment from 'moment/moment'; -import { EVENT as EVENT_ORDER } from '../domain/Order'; -import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; -import { navigateAndLoadTasks } from '../redux/Courier/taskActions'; - -export default function usePushNotification() { - const dispatch = useDispatch(); - - const onRegister = useCallback( - token => { - console.log('onRegister token:', token); - dispatch(registerPushNotificationToken(token)); - }, - [dispatch], - ); - - /** - * called when a user taps on a notification in the notification center - * android: notification is only shown when the app is in the background - * ios: notification is shown both in the foreground and background - */ - const onNotification = useCallback( - message => { - console.log('onNotification message:', message); - - const { event } = message.data; - - if (event && event.name === EVENT_ORDER.CREATED) { - tracker.logEvent( - analyticsEvent.restaurant._category, - analyticsEvent.restaurant.orderCreatedMessage, - message.foreground ? 'in_app' : 'notification_center', - ); - - const { order } = event.data; - - // Here in any case, we navigate to the order that was tapped, - // it should have been loaded via WebSocket already. - dispatch(loadOrderAndNavigate(order)); - } - - if (event && event.name === EVENT_TASK_COLLECTION.CHANGED) { - tracker.logEvent( - analyticsEvent.courier._category, - analyticsEvent.courier.tasksChangedMessage, - message.foreground ? 'in_app' : 'notification_center', - ); - - if (message.foreground) { - dispatch( - foregroundPushNotification(event.name, { - date: event.data.date, - }), - ); - } else { - dispatch(navigateAndLoadTasks(moment(event.data.date))); - } - } - }, - [dispatch], - ); - - /** - * called when a push notification is received while the app is in the foreground - * android only! - */ - const onBackgroundMessage = useCallback( - message => { - console.log('onBackgroundMessage message:', message.data); - - const { event } = message.data; - - if (event) { - switch (event.name) { - case EVENT_ORDER.CREATED: - dispatch( - loadOrder(event.data.order, order => { - if (order) { - dispatch( - foregroundPushNotification(event.name, { - order: order, - }), - ); - } - }), - ); - break; - case EVENT_TASK_COLLECTION.CHANGED: - dispatch( - foregroundPushNotification(event.name, { - date: event.data.date, - }), - ); - break; - default: - break; - } - } - }, - [dispatch], - ); - - useEffect(() => { - PushNotification.configure({ - onRegister: token => onRegister(token), - onNotification: message => onNotification(message), - onBackgroundMessage: message => onBackgroundMessage(message), - }); - - return () => { - PushNotification.removeListeners(); - }; - }, [onBackgroundMessage, onNotification, onRegister]); -} diff --git a/src/redux/middlewares/PushNotificationMiddleware/index.js b/src/redux/middlewares/PushNotificationMiddleware/index.js index d0847278d..1aaa1b7fb 100644 --- a/src/redux/middlewares/PushNotificationMiddleware/index.js +++ b/src/redux/middlewares/PushNotificationMiddleware/index.js @@ -2,6 +2,8 @@ import { Platform } from 'react-native'; import { LOGOUT_REQUEST, deletePushNotificationTokenSuccess, + foregroundPushNotification, + registerPushNotificationToken, savePushNotificationTokenSuccess, } from '../../App/actions'; import { @@ -9,6 +11,14 @@ import { selectHttpClientHasCredentials, selectIsAuthenticated, } from '../../App/selectors'; +import PushNotification from '../../../notifications'; +import { EVENT as EVENT_ORDER } from '../../../domain/Order'; +import tracker from '../../../analytics/Tracker'; +import analyticsEvent from '../../../analytics/Event'; +import { loadOrder, loadOrderAndNavigate } from '../../Restaurant/actions'; +import { EVENT as EVENT_TASK_COLLECTION } from '../../../domain/TaskCollection'; +import { navigateAndLoadTasks } from '../../Courier/taskActions'; +import moment from 'moment'; let isFetching = false; @@ -17,6 +27,97 @@ let isFetching = false; // (for example, when app is launched for the first time) // We store the token for later, when the user authenticates export default ({ getState, dispatch }) => { + const onRegister = token => { + console.log('onRegister token:', token); + dispatch(registerPushNotificationToken(token)); + }; + + /** + * called when a user taps on a notification in the notification center + * android: notification is only shown when the app is in the background + * ios: notification is shown both in the foreground and background + */ + const onNotification = message => { + console.log('onNotification message:', message); + + const { event } = message.data; + + if (event && event.name === EVENT_ORDER.CREATED) { + tracker.logEvent( + analyticsEvent.restaurant._category, + analyticsEvent.restaurant.orderCreatedMessage, + message.foreground ? 'in_app' : 'notification_center', + ); + + const { order } = event.data; + + // Here in any case, we navigate to the order that was tapped, + // it should have been loaded via WebSocket already. + dispatch(loadOrderAndNavigate(order)); + } + + if (event && event.name === EVENT_TASK_COLLECTION.CHANGED) { + tracker.logEvent( + analyticsEvent.courier._category, + analyticsEvent.courier.tasksChangedMessage, + message.foreground ? 'in_app' : 'notification_center', + ); + + if (message.foreground) { + dispatch( + foregroundPushNotification(event.name, { + date: event.data.date, + }), + ); + } else { + dispatch(navigateAndLoadTasks(moment(event.data.date))); + } + } + }; + + /** + * called when a push notification is received while the app is in the foreground + * android only! + */ + const onBackgroundMessage = message => { + console.log('onBackgroundMessage message:', message.data); + + const { event } = message.data; + + if (event) { + switch (event.name) { + case EVENT_ORDER.CREATED: + dispatch( + loadOrder(event.data.order, order => { + if (order) { + dispatch( + foregroundPushNotification(event.name, { + order: order, + }), + ); + } + }), + ); + break; + case EVENT_TASK_COLLECTION.CHANGED: + dispatch( + foregroundPushNotification(event.name, { + date: event.data.date, + }), + ); + break; + default: + break; + } + } + }; + + PushNotification.configure({ + onRegister: token => onRegister(token), + onNotification: message => onNotification(message), + onBackgroundMessage: message => onBackgroundMessage(message), + }); + return next => action => { const result = next(action); const state = getState(); From 99e14d16e7e4066e590f17c87c80847bc97a187d Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:05:28 -0700 Subject: [PATCH 30/69] deleted empty files --- src/redux/Restaurant/__tests__/middlewares.test.js | 0 src/redux/Restaurant/middlewares.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/redux/Restaurant/__tests__/middlewares.test.js delete mode 100644 src/redux/Restaurant/middlewares.js diff --git a/src/redux/Restaurant/__tests__/middlewares.test.js b/src/redux/Restaurant/__tests__/middlewares.test.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/redux/Restaurant/middlewares.js b/src/redux/Restaurant/middlewares.js deleted file mode 100644 index e69de29bb..000000000 From 69b2198c84f4ea4a489f2e5601e28bfa15e90c01 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:12:43 -0700 Subject: [PATCH 31/69] import createAction from redux/toolkit --- src/redux/App/actions.js | 97 ++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js index 6b155df1d..4fb7b3019 100644 --- a/src/redux/App/actions.js +++ b/src/redux/App/actions.js @@ -1,5 +1,6 @@ +import { createAction } from '@reduxjs/toolkit'; +import { createAction as createFsAction } from 'redux-actions'; import { CommonActions } from '@react-navigation/native'; -import { createAction } from 'redux-actions'; import analyticsEvent from '../../analytics/Event'; import tracker from '../../analytics/Tracker'; import userProperty from '../../analytics/UserProperty'; @@ -112,105 +113,105 @@ export const SET_SPINNER_DELAY_ENABLED = '@app/SET_IS_SPINNER_DELAY_ENABLED'; * Action Creators */ -export const setLoading = createAction(SET_LOADING); +export const setLoading = createFsAction(SET_LOADING); -export const foregroundPushNotification = createAction( +export const foregroundPushNotification = createFsAction( FOREGROUND_PUSH_NOTIFICATION, (event, params = {}) => ({ event, params }), ); -export const addNotification = createAction( +export const addNotification = createFsAction( ADD_NOTIFICATION, (event, params = {}) => ({ event, params }), ); -export const clearNotifications = createAction(CLEAR_NOTIFICATIONS); +export const clearNotifications = createFsAction(CLEAR_NOTIFICATIONS); -export const _authenticationRequest = createAction(AUTHENTICATION_REQUEST); -export const _authenticationSuccess = createAction(AUTHENTICATION_SUCCESS); -const _authenticationFailure = createAction(AUTHENTICATION_FAILURE); +export const _authenticationRequest = createFsAction(AUTHENTICATION_REQUEST); +export const _authenticationSuccess = createFsAction(AUTHENTICATION_SUCCESS); +const _authenticationFailure = createFsAction(AUTHENTICATION_FAILURE); -export const clearAuthenticationErrors = createAction( +export const clearAuthenticationErrors = createFsAction( CLEAR_AUTHENTICATION_ERRORS, ); -const resetPasswordInit = createAction(RESET_PASSWORD_INIT); -const resetPasswordRequest = createAction(RESET_PASSWORD_REQUEST); -const resetPasswordRequestSuccess = createAction( +const resetPasswordInit = createFsAction(RESET_PASSWORD_INIT); +const resetPasswordRequest = createFsAction(RESET_PASSWORD_REQUEST); +const resetPasswordRequestSuccess = createFsAction( RESET_PASSWORD_REQUEST_SUCCESS, ); -const resetPasswordRequestFailure = createAction( +const resetPasswordRequestFailure = createFsAction( RESET_PASSWORD_REQUEST_FAILURE, ); -export const logoutRequest = createAction(LOGOUT_REQUEST); -export const _logoutSuccess = createAction(LOGOUT_SUCCESS); -export const setServers = createAction(SET_SERVERS); +export const logoutRequest = createFsAction(LOGOUT_REQUEST); +export const _logoutSuccess = createFsAction(LOGOUT_SUCCESS); +export const setServers = createFsAction(SET_SERVERS); -const setUser = createAction(SET_USER); -const _setBaseURL = createAction(SET_BASE_URL); -const _setCurrentRoute = createAction(SET_CURRENT_ROUTE); -const _setSelectServerError = createAction(SET_SELECT_SERVER_ERROR); -const _clearSelectServerError = createAction(CLEAR_SELECT_SERVER_ERROR); +const setUser = createFsAction(SET_USER); +const _setBaseURL = createFsAction(SET_BASE_URL); +const _setCurrentRoute = createFsAction(SET_CURRENT_ROUTE); +const _setSelectServerError = createFsAction(SET_SELECT_SERVER_ERROR); +const _clearSelectServerError = createFsAction(CLEAR_SELECT_SERVER_ERROR); -const _resumeCheckoutAfterActivation = createAction( +const _resumeCheckoutAfterActivation = createFsAction( RESUME_CHECKOUT_AFTER_ACTIVATION, ); -export const registerPushNotificationToken = createAction( +export const registerPushNotificationToken = createFsAction( REGISTER_PUSH_NOTIFICATION_TOKEN, ); -export const savePushNotificationTokenSuccess = createAction( +export const savePushNotificationTokenSuccess = createFsAction( SAVE_PUSH_NOTIFICATION_TOKEN_SUCCESS, ); -export const deletePushNotificationTokenSuccess = createAction( +export const deletePushNotificationTokenSuccess = createFsAction( DELETE_PUSH_NOTIFICATION_TOKEN_SUCCESS, ); -const _loadMyStoresSuccess = createAction(LOAD_MY_STORES_SUCCESS); +const _loadMyStoresSuccess = createFsAction(LOAD_MY_STORES_SUCCESS); -const loadMyRestaurantsRequest = createAction(LOAD_MY_RESTAURANTS_REQUEST); -const loadMyRestaurantsSuccess = createAction(LOAD_MY_RESTAURANTS_SUCCESS); -const loadMyRestaurantsFailure = createAction(LOAD_MY_RESTAURANTS_FAILURE); +const loadMyRestaurantsRequest = createFsAction(LOAD_MY_RESTAURANTS_REQUEST); +const loadMyRestaurantsSuccess = createFsAction(LOAD_MY_RESTAURANTS_SUCCESS); +const loadMyRestaurantsFailure = createFsAction(LOAD_MY_RESTAURANTS_FAILURE); -const setSettings = createAction(SET_SETTINGS); +const setSettings = createFsAction(SET_SETTINGS); -export const setInternetReachable = createAction(SET_INTERNET_REACHABLE); +export const setInternetReachable = createFsAction(SET_INTERNET_REACHABLE); -export const setBackgroundGeolocationEnabled = createAction( +export const setBackgroundGeolocationEnabled = createFsAction( SET_BACKGROUND_GEOLOCATION_ENABLED, ); -export const backgroundPermissionDisclosed = createAction( +export const backgroundPermissionDisclosed = createFsAction( BACKGROUND_PERMISSION_DISCLOSED, ); -export const setModal = createAction(SET_MODAL); -export const resetModal = createAction(RESET_MODAL); -export const closeModal = createAction(CLOSE_MODAL); -export const onboarded = createAction(ONBOARDED); +export const setModal = createFsAction(SET_MODAL); +export const resetModal = createFsAction(RESET_MODAL); +export const closeModal = createFsAction(CLOSE_MODAL); +export const onboarded = createFsAction(ONBOARDED); -export const acceptTermsAndConditions = createAction( +export const acceptTermsAndConditions = createFsAction( ACCEPT_TERMS_AND_CONDITIONS, ); -export const acceptPrivacyPolicy = createAction(ACCEPT_PRIVACY_POLICY); +export const acceptPrivacyPolicy = createFsAction(ACCEPT_PRIVACY_POLICY); -const loadTermsAndConditionsRequest = createAction( +const loadTermsAndConditionsRequest = createFsAction( LOAD_TERMS_AND_CONDITIONS_REQUEST, ); -const loadTermsAndConditionsSuccess = createAction( +const loadTermsAndConditionsSuccess = createFsAction( LOAD_TERMS_AND_CONDITIONS_SUCCESS, ); -const loadTermsAndConditionsFailure = createAction( +const loadTermsAndConditionsFailure = createFsAction( LOAD_TERMS_AND_CONDITIONS_FAILURE, ); -const loadPrivacyPolicyRequest = createAction(LOAD_PRIVACY_POLICY_REQUEST); -const loadPrivacyPolicySuccess = createAction(LOAD_PRIVACY_POLICY_SUCCESS); -const loadPrivacyPolicyFailure = createAction(LOAD_PRIVACY_POLICY_FAILURE); +const loadPrivacyPolicyRequest = createFsAction(LOAD_PRIVACY_POLICY_REQUEST); +const loadPrivacyPolicySuccess = createFsAction(LOAD_PRIVACY_POLICY_SUCCESS); +const loadPrivacyPolicyFailure = createFsAction(LOAD_PRIVACY_POLICY_FAILURE); -const registrationErrors = createAction(REGISTRATION_ERRORS); -const loginByEmailErrors = createAction(LOGIN_BY_EMAIL_ERRORS); +const registrationErrors = createFsAction(REGISTRATION_ERRORS); +const loginByEmailErrors = createFsAction(LOGIN_BY_EMAIL_ERRORS); -export const setSpinnerDelayEnabled = createAction(SET_SPINNER_DELAY_ENABLED); +export const setSpinnerDelayEnabled = createFsAction(SET_SPINNER_DELAY_ENABLED); function setBaseURL(baseURL) { return (dispatch, getState) => { From 84ed7c17d7f100d0e74474b7c02674812e048399 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:13:09 -0700 Subject: [PATCH 32/69] moved isFetching inside init block --- src/redux/middlewares/PushNotificationMiddleware/index.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/redux/middlewares/PushNotificationMiddleware/index.js b/src/redux/middlewares/PushNotificationMiddleware/index.js index 1aaa1b7fb..074c6013d 100644 --- a/src/redux/middlewares/PushNotificationMiddleware/index.js +++ b/src/redux/middlewares/PushNotificationMiddleware/index.js @@ -1,4 +1,5 @@ import { Platform } from 'react-native'; +import moment from 'moment'; import { LOGOUT_REQUEST, deletePushNotificationTokenSuccess, @@ -18,9 +19,6 @@ import analyticsEvent from '../../../analytics/Event'; import { loadOrder, loadOrderAndNavigate } from '../../Restaurant/actions'; import { EVENT as EVENT_TASK_COLLECTION } from '../../../domain/TaskCollection'; import { navigateAndLoadTasks } from '../../Courier/taskActions'; -import moment from 'moment'; - -let isFetching = false; // As remote push notifications are configured very early, // most of the time the user won't be authenticated @@ -118,6 +116,8 @@ export default ({ getState, dispatch }) => { onBackgroundMessage: message => onBackgroundMessage(message), }); + let isFetching = false; + return next => action => { const result = next(action); const state = getState(); @@ -161,7 +161,6 @@ export default ({ getState, dispatch }) => { isFetching = false; }); } - return result; }; }; From d977c9ae3c928c454431d78049b4d899f6c29b04 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:14:24 -0700 Subject: [PATCH 33/69] manage sound inside redux middleware --- src/components/NotificationHandler.js | 93 ++++++++----------- src/hooks/usePlayNotificationSound.js | 58 ------------ src/redux/App/actions.js | 3 + src/redux/App/selectors.js | 40 +++++++- .../middlewares/SoundMiddleware/index.js | 50 ++++++++++ src/redux/store.js | 2 + 6 files changed, 135 insertions(+), 111 deletions(-) delete mode 100644 src/hooks/usePlayNotificationSound.js create mode 100644 src/redux/middlewares/SoundMiddleware/index.js diff --git a/src/components/NotificationHandler.js b/src/components/NotificationHandler.js index 06d114187..743870987 100644 --- a/src/components/NotificationHandler.js +++ b/src/components/NotificationHandler.js @@ -1,74 +1,63 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { clearNotifications } from '../redux/App/actions'; +import { + clearNotifications, + startSound, + stopSound, +} from '../redux/App/actions'; import NotificationModal from './NotificationModal'; -import usePlayNotificationSound from '../hooks/usePlayNotificationSound'; -import { selectNotifications } from '../redux/App/selectors'; -import { EVENT as EVENT_ORDER } from '../domain/Order'; -import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; -import { selectTasksChangedAlertSound } from '../redux/Courier'; -import { selectAutoAcceptOrdersEnabled } from '../redux/Restaurant/selectors'; +import { + selectNotificationsToDisplay, + selectNotificationsWithSound, +} from '../redux/App/selectors'; +import { AppState } from 'react-native'; + +const NOTIFICATION_DURATION_MS = 10000; /** * This component is used - * 1/ To show notifications when the app is in foreground (see NotificationModal) - * 2/ To play a sound when a notification is received (see usePlayNotificationSound) + * 1/ To show notifications when the app is in the foreground (using NotificationModal) + * 2/ To play a sound when a notification is received (via SoundMiddleware) + * + * Push notifications are configured and received in PushNotificationMiddleware */ export default function NotificationHandler() { - const allNotifications = useSelector(selectNotifications); - - const tasksChangedAlertSound = useSelector(selectTasksChangedAlertSound); - - const notificationsWithSound = allNotifications.filter(notification => { - switch (notification.event) { - case EVENT_ORDER.CREATED: - return true; - case EVENT_TASK_COLLECTION.CHANGED: - return tasksChangedAlertSound; - default: - return false; - } - }); - - const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled); - - const notificationsToDisplay = allNotifications.filter(notification => { - switch (notification.event) { - case EVENT_ORDER.CREATED: - return !autoAcceptOrdersEnabled; - case EVENT_TASK_COLLECTION.CHANGED: - return true; - default: - return true; - } - }); - - const { isSoundPlaying, stopSound } = usePlayNotificationSound( - notificationsWithSound, - ); + const notificationsToDisplay = useSelector(selectNotificationsToDisplay); + const notificationsWithSound = useSelector(selectNotificationsWithSound); const dispatch = useDispatch(); - const clear = useCallback(() => { - stopSound(); - dispatch(clearNotifications()); - }, [stopSound, dispatch]); - useEffect(() => { - if (isSoundPlaying) { - // Clear notifications after 10 seconds + if ( + notificationsToDisplay.length > 0 || + notificationsWithSound.length > 0 + ) { setTimeout(() => { - clear(); - }, 10000); + dispatch(clearNotifications()); + }, NOTIFICATION_DURATION_MS); + } + }, [notificationsToDisplay, notificationsWithSound, dispatch]); + + useEffect(() => { + // on Android, when notification is received, OS let us execute some code + // but it's very limited, e.g. handlers set via setTimeout are not executed + // so we do not play sound in that case, because we will not be able to stop it + if ( + notificationsWithSound.length > 0 && + AppState.currentState === 'active' + ) { + dispatch(startSound()); + } else { + dispatch(stopSound()); } - }, [isSoundPlaying, clear]); + }, [notificationsWithSound, dispatch]); return ( { - clear(); + dispatch(clearNotifications()); }} /> ); diff --git a/src/hooks/usePlayNotificationSound.js b/src/hooks/usePlayNotificationSound.js deleted file mode 100644 index 08c4daa59..000000000 --- a/src/hooks/usePlayNotificationSound.js +++ /dev/null @@ -1,58 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import Sound from 'react-native-sound'; - -// Make sure sound will play even when device is in silent mode -Sound.setCategory('Playback'); - -export default function usePlayNotificationSound(notifications) { - const [sound, setSound] = useState(null); - const [isSoundReady, setIsSoundReady] = useState(false); - const [isSoundPlaying, setIsSoundPlaying] = useState(false); - - // load sound - useEffect(() => { - const bell = new Sound( - 'misstickle__indian_bell_chime.wav', - Sound.MAIN_BUNDLE, - error => { - if (error) { - return; - } - - bell.setNumberOfLoops(-1); - - setSound(bell); - setIsSoundReady(true); - }, - ); - }, []); - - const startSound = useCallback(() => { - if (isSoundReady && !isSoundPlaying) { - sound.play(success => { - if (!success) { - sound.reset(); - } - }); - - setIsSoundPlaying(true); - } - }, [isSoundReady, isSoundPlaying, sound]); - - const stopSound = useCallback(() => { - if (isSoundReady && isSoundPlaying) { - sound.stop(() => {}); - setIsSoundPlaying(false); - } - }, [isSoundPlaying, isSoundReady, sound]); - - useEffect(() => { - if (notifications.length > 0) { - startSound(); - } else { - stopSound(); - } - }, [notifications, startSound, stopSound]); - - return { isSoundPlaying, stopSound }; -} diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js index 4fb7b3019..e14a74717 100644 --- a/src/redux/App/actions.js +++ b/src/redux/App/actions.js @@ -213,6 +213,9 @@ const loginByEmailErrors = createFsAction(LOGIN_BY_EMAIL_ERRORS); export const setSpinnerDelayEnabled = createFsAction(SET_SPINNER_DELAY_ENABLED); +export const startSound = createAction('START_SOUND'); +export const stopSound = createAction('STOP_SOUND'); + function setBaseURL(baseURL) { return (dispatch, getState) => { dispatch(_setBaseURL(baseURL)); diff --git a/src/redux/App/selectors.js b/src/redux/App/selectors.js index 04820bd7c..2fc71e2fb 100644 --- a/src/redux/App/selectors.js +++ b/src/redux/App/selectors.js @@ -1,8 +1,14 @@ import _ from 'lodash'; import { createSelector } from 'reselect'; -import { selectIsTasksLoading } from '../Courier/taskSelectors'; +import { + selectIsTasksLoading, + selectTasksChangedAlertSound, +} from '../Courier/taskSelectors'; import { selectIsDispatchFetching } from '../Dispatch/selectors'; +import { EVENT as EVENT_ORDER } from '../../domain/Order'; +import { EVENT as EVENT_TASK_COLLECTION } from '../../domain/TaskCollection'; +import { selectAutoAcceptOrdersEnabled } from '../Restaurant/selectors'; export const selectUser = state => state.app.user; export const selectHttpClient = state => state.app.httpClient; @@ -151,3 +157,35 @@ export const selectIsSpinnerDelayEnabled = state => export const selectCurrentRoute = state => state.app.currentRoute; export const selectNotifications = state => state.app.notifications; + +export const selectNotificationsWithSound = createSelector( + selectNotifications, + selectTasksChangedAlertSound, + (notifications, tasksChangedAlertSound) => + notifications.filter(notification => { + switch (notification.event) { + case EVENT_ORDER.CREATED: + return true; + case EVENT_TASK_COLLECTION.CHANGED: + return tasksChangedAlertSound; + default: + return false; + } + }), +); + +export const selectNotificationsToDisplay = createSelector( + selectNotifications, + selectAutoAcceptOrdersEnabled, + (notifications, autoAcceptOrdersEnabled) => + notifications.filter(notification => { + switch (notification.event) { + case EVENT_ORDER.CREATED: + return !autoAcceptOrdersEnabled; + case EVENT_TASK_COLLECTION.CHANGED: + return true; + default: + return true; + } + }), +); diff --git a/src/redux/middlewares/SoundMiddleware/index.js b/src/redux/middlewares/SoundMiddleware/index.js new file mode 100644 index 000000000..0cd5584f4 --- /dev/null +++ b/src/redux/middlewares/SoundMiddleware/index.js @@ -0,0 +1,50 @@ +import Sound from 'react-native-sound'; +import { startSound, stopSound } from '../../App/actions'; + +// Make sure sound will play even when device is in silent mode +Sound.setCategory('Playback'); + +export default ({ getState, dispatch }) => { + let isSoundReady = false; + let isSoundPlaying = false; + const bell = new Sound( + 'misstickle__indian_bell_chime.wav', + Sound.MAIN_BUNDLE, + error => { + if (error) { + return; + } + + bell.setNumberOfLoops(-1); + isSoundReady = true; + }, + ); + + return next => action => { + const result = next(action); + + if (action.type === startSound.type) { + if (isSoundReady && !isSoundPlaying) { + isSoundPlaying = true; + bell.play(success => { + if (!success) { + bell.reset(); + } + }); + } + + return result; + } + + if (action.type === stopSound.type) { + if (isSoundPlaying) { + bell.stop(() => {}); + isSoundPlaying = false; + } + + return result; + } + + return result; + }; +}; diff --git a/src/redux/store.js b/src/redux/store.js index d1cdbddf2..12258dc59 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -16,6 +16,7 @@ import SentryMiddleware from './middlewares/SentryMiddleware'; import { ringOnTaskListUpdated } from './Courier/taskMiddlewares'; import CentrifugoMiddleware from './middlewares/CentrifugoMiddleware'; import { filterExpiredCarts } from './Checkout/middlewares'; +import SoundMiddleware from './middlewares/SoundMiddleware'; const middlewares = [ thunk, @@ -26,6 +27,7 @@ const middlewares = [ CentrifugoMiddleware, SentryMiddleware, filterExpiredCarts, + SoundMiddleware, ]; if (!Config.DEFAULT_SERVER) { From 21ba9b6a271e8cde8134dc1b2ca36b07aec45b9a Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:53:11 -0700 Subject: [PATCH 34/69] put back 'rindOnOrderCreated' middleware --- src/redux/App/reducers.js | 13 -- .../Restaurant/__tests__/middlewares.test.js | 191 ++++++++++++++++++ src/redux/Restaurant/middlewares.js | 53 +++++ src/redux/store.js | 8 +- 4 files changed, 251 insertions(+), 14 deletions(-) create mode 100644 src/redux/Restaurant/__tests__/middlewares.test.js create mode 100644 src/redux/Restaurant/middlewares.js diff --git a/src/redux/App/reducers.js b/src/redux/App/reducers.js index 33621a0a9..60bd6fc91 100644 --- a/src/redux/App/reducers.js +++ b/src/redux/App/reducers.js @@ -182,19 +182,6 @@ export default (state = initialState, action = {}) => { isCentrifugoConnected: false, }; - case CENTRIFUGO_MESSAGE: { - const { name, data } = action.payload; - - switch (name) { - case EVENT_ORDER.CREATED: - return updateNotifications(state, name, data); - // case EVENT_TASK_COLLECTION.CHANGED: - // this event is currently handled by taskMiddlewares; maybe we should move it here - default: - return state; - } - } - case FOREGROUND_PUSH_NOTIFICATION: { const { event, params } = action.payload; diff --git a/src/redux/Restaurant/__tests__/middlewares.test.js b/src/redux/Restaurant/__tests__/middlewares.test.js new file mode 100644 index 000000000..e6d7df7b1 --- /dev/null +++ b/src/redux/Restaurant/__tests__/middlewares.test.js @@ -0,0 +1,191 @@ +import { applyMiddleware, combineReducers, createStore } from 'redux'; +import thunk from 'redux-thunk'; +import AppUser from '../../../AppUser'; +import appReducer from '../../App/reducers'; +import { message as wsMessage } from '../../middlewares/CentrifugoMiddleware/actions'; +import { loadOrderSuccess, loadOrdersSuccess } from '../actions'; +import { notifyOnNewOrderCreated } from '../middlewares'; +import restaurantReducer from '../reducers'; + +describe('notifyOnNewOrderCreated', () => { + beforeEach(() => { + jest.mock('react-native/Libraries/AppState/AppState', () => ({ + currentState: 'active', + })); + }); + + it('does nothing with action type "LOAD_ORDERS_SUCCESS"', () => { + const preloadedState = { + app: { + notifications: [], + }, + restaurant: { + orders: [], + }, + }; + + const reducer = combineReducers({ + app: appReducer, + restaurant: restaurantReducer, + }); + + const store = createStore( + reducer, + preloadedState, + applyMiddleware(notifyOnNewOrderCreated), + ); + + store.dispatch( + loadOrdersSuccess([{ '@id': '/api/orders/1', state: 'new' }]), + ); + + const newState = store.getState(); + + expect(newState).toMatchObject({ + app: { + notifications: [], + }, + restaurant: { + orders: [{ '@id': '/api/orders/1', state: 'new' }], + }, + }); + }); + + it('pushes new notification with action type "LOAD_ORDER_SUCCESS"', () => { + const preloadedState = { + app: { + notifications: [], + user: new AppUser( + 'bob', + 'bob@example.com', + 'abc123456', + ['ROLE_RESTAURANT'], + '', + ), + }, + restaurant: { + orders: [], + }, + }; + + const reducer = combineReducers({ + app: appReducer, + restaurant: restaurantReducer, + }); + + const store = createStore( + reducer, + preloadedState, + applyMiddleware(notifyOnNewOrderCreated), + ); + + store.dispatch(loadOrderSuccess({ '@id': '/api/orders/1', state: 'new' })); + + const newState = store.getState(); + + expect(newState).toMatchObject({ + app: { + notifications: [ + { + event: 'order:created', + params: { + order: { '@id': '/api/orders/1', state: 'new' }, + }, + }, + ], + }, + restaurant: { + orders: [{ '@id': '/api/orders/1', state: 'new' }], + }, + }); + }); + + it('pushes new notification with action type "MESSAGE"', () => { + const preloadedState = { + app: { + notifications: [], + user: new AppUser( + 'bob', + 'bob@example.com', + 'abc123456', + ['ROLE_RESTAURANT'], + '', + ), + }, + restaurant: { + orders: [], + }, + }; + + const reducer = combineReducers({ + app: appReducer, + restaurant: restaurantReducer, + }); + + const store = createStore( + reducer, + preloadedState, + applyMiddleware(thunk, notifyOnNewOrderCreated), + ); + + store.dispatch( + wsMessage({ + name: 'order:created', + data: { order: { '@id': '/api/orders/1', state: 'new' } }, + }), + ); + + const newState = store.getState(); + + expect(newState).toMatchObject({ + app: { + notifications: [ + { + event: 'order:created', + params: { + order: { '@id': '/api/orders/1', state: 'new' }, + }, + }, + ], + }, + restaurant: { + orders: [{ '@id': '/api/orders/1', state: 'new' }], + }, + }); + }); + + it('does nothing when order is already loaded', () => { + const preloadedState = { + app: { + notifications: [], + }, + restaurant: { + orders: [{ '@id': '/api/orders/1', state: 'new' }], + }, + }; + + const reducer = combineReducers({ + app: appReducer, + restaurant: restaurantReducer, + }); + + const store = createStore( + reducer, + preloadedState, + applyMiddleware(notifyOnNewOrderCreated), + ); + + store.dispatch(loadOrderSuccess({ '@id': '/api/orders/1', state: 'new' })); + + const newState = store.getState(); + + expect(newState).toMatchObject({ + app: { + notifications: [], + }, + restaurant: { + orders: [{ '@id': '/api/orders/1', state: 'new' }], + }, + }); + }); +}); diff --git a/src/redux/Restaurant/middlewares.js b/src/redux/Restaurant/middlewares.js new file mode 100644 index 000000000..a1cc42749 --- /dev/null +++ b/src/redux/Restaurant/middlewares.js @@ -0,0 +1,53 @@ +import _ from 'lodash'; +import { AppState } from 'react-native'; + +import { addNotification } from '../App/actions'; +import { selectUser } from '../App/selectors'; +import { LOAD_ORDERS_SUCCESS } from './actions'; +import { EVENT as EVENT_ORDER, STATE } from '../../domain/Order'; + +export const notifyOnNewOrderCreated = ({ getState, dispatch }) => { + return next => action => { + if (AppState.currentState !== 'active') { + return next(action); + } + + // Avoid ringing on first load + if (action.type === LOAD_ORDERS_SUCCESS) { + return next(action); + } + + const prevState = getState(); + const result = next(action); + const state = getState(); + + const user = selectUser(state); + const shouldShowAlert = + user && + user.isAuthenticated() && + (user.hasRole('ROLE_ADMIN') || user.hasRole('ROLE_RESTAURANT')); + + if (!shouldShowAlert) { + return result; + } + + if (state.restaurant.orders.length > 0) { + if ( + state.restaurant.orders.length !== prevState.restaurant.orders.length + ) { + const orders = _.differenceWith( + state.restaurant.orders, + prevState.restaurant.orders, + (a, b) => a['@id'] + ':' + a.state === b['@id'] + ':' + b.state, + ); + orders.forEach(o => { + if (o.state === STATE.NEW) { + dispatch(addNotification(EVENT_ORDER.CREATED, { order: o })); + } + }); + } + } + + return result; + }; +}; diff --git a/src/redux/store.js b/src/redux/store.js index 12258dc59..12bb7962b 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -17,6 +17,7 @@ import { ringOnTaskListUpdated } from './Courier/taskMiddlewares'; import CentrifugoMiddleware from './middlewares/CentrifugoMiddleware'; import { filterExpiredCarts } from './Checkout/middlewares'; import SoundMiddleware from './middlewares/SoundMiddleware'; +import { notifyOnNewOrderCreated } from './Restaurant/middlewares'; const middlewares = [ thunk, @@ -32,7 +33,12 @@ const middlewares = [ if (!Config.DEFAULT_SERVER) { middlewares.push( - ...[GeolocationMiddleware, BluetoothMiddleware, ringOnTaskListUpdated], + ...[ + GeolocationMiddleware, + BluetoothMiddleware, + notifyOnNewOrderCreated, + ringOnTaskListUpdated, + ], ); } From c58d342aab74660629d04964a079dcadacbfce0c Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 21 May 2024 15:00:33 -0700 Subject: [PATCH 35/69] bump version code --- android/app/build.gradle | 4 ++-- ios/CoopCycle/Info.plist | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index a66972802..17ffb6d22 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled true - versionCode 204 - versionName "2.17.0" + versionCode 205 + versionName "2.17.1" manifestPlaceholders = [ tipsiStripeRedirectScheme: "coopcycle", diff --git a/ios/CoopCycle/Info.plist b/ios/CoopCycle/Info.plist index fda955146..5d246af27 100644 --- a/ios/CoopCycle/Info.plist +++ b/ios/CoopCycle/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.16.4 + 2.17.1 CFBundleSignature ???? CFBundleURLTypes @@ -37,7 +37,7 @@ CFBundleVersion - 146 + 205 FacebookAppID 1303550106471840 FacebookClientToken From 08291f094295dca3eacb454f1c4d3da3ea94e57e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 21 May 2024 16:48:09 -0700 Subject: [PATCH 36/69] added: display buildVersion --- src/navigation/components/DrawerContent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navigation/components/DrawerContent.js b/src/navigation/components/DrawerContent.js index 67d9a6773..17dad7476 100644 --- a/src/navigation/components/DrawerContent.js +++ b/src/navigation/components/DrawerContent.js @@ -252,7 +252,7 @@ class DrawerContent extends Component {
- {VersionNumber.appVersion} + {`${VersionNumber.appVersion} (${VersionNumber.buildVersion})`}
From 7e916c0e86e11006424e4c7429b3ee74db045cce Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 21 May 2024 16:48:30 -0700 Subject: [PATCH 37/69] bump version code --- android/app/build.gradle | 4 ++-- ios/CoopCycle/Info.plist | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 17ffb6d22..03c6642b5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled true - versionCode 205 - versionName "2.17.1" + versionCode 206 + versionName "2.17.2" manifestPlaceholders = [ tipsiStripeRedirectScheme: "coopcycle", diff --git a/ios/CoopCycle/Info.plist b/ios/CoopCycle/Info.plist index 5d246af27..6473569a6 100644 --- a/ios/CoopCycle/Info.plist +++ b/ios/CoopCycle/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.17.1 + 2.17.2 CFBundleSignature ???? CFBundleURLTypes @@ -37,7 +37,7 @@ CFBundleVersion - 205 + 206 FacebookAppID 1303550106471840 FacebookClientToken From e7abc3c2798027b092bd3f3727a0075831a1a9fc Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 21 May 2024 17:16:44 -0700 Subject: [PATCH 38/69] (temporary) disable instance (workflow inputs limit) --- .github/workflows/build_android.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index b06875cd3..ff31ccad1 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -28,10 +28,10 @@ on: description: 'Build Naofood app' required: true type: boolean - build_zampate: - description: 'Build Zampate app' - required: true - type: boolean +# build_zampate: +# description: 'Build Zampate app' +# required: true +# type: boolean build_kooglof: description: 'Build Kooglof app' required: true From 22250d4172d1c8cabc081b315d56f7ab088cc8f1 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 21 May 2024 17:20:50 -0700 Subject: [PATCH 39/69] changed: checkout tag instead of a branch --- .github/workflows/build_android.yml | 13 +++++++++++++ .github/workflows/fastlane_android.yml | 12 ++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index ff31ccad1..584a8e01a 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -2,6 +2,10 @@ name: Build Android on: workflow_dispatch: inputs: + tag: + type: string + description: Deploy a specific git tag + required: true deploy_google_play: description: 'Deploy to Google Play' required: true @@ -51,6 +55,7 @@ jobs: uses: ./.github/workflows/fastlane_android.yml secrets: inherit with: + tag: ${{ inputs.tag }} google_play_track: ${{ inputs.google_play_track }} deploy_google_play: ${{ inputs.deploy_google_play }} default_beta: @@ -58,6 +63,7 @@ jobs: name: Build default beta app uses: ./.github/workflows/fastlane_android.yml with: + tag: ${{ inputs.tag }} instance: beta app_name: CoopCycle (Beta) package_name: fr.coopcycle.beta @@ -71,6 +77,7 @@ jobs: name: Build Naofood app uses: ./.github/workflows/fastlane_android.yml with: + tag: ${{ inputs.tag }} instance: naofood instance_url: https://naofood.coopcycle.org app_name: Naofood @@ -86,6 +93,7 @@ jobs: name: Build Zampate app uses: ./.github/workflows/fastlane_android.yml with: + tag: ${{ inputs.tag }} instance: zampate instance_url: https://zampate.coopcycle.org app_name: Zampate @@ -101,6 +109,7 @@ jobs: name: Build Les Coursiers Stéphanois app uses: ./.github/workflows/fastlane_android.yml with: + tag: ${{ inputs.tag }} instance: coursiers_stephanois instance_url: https://coursiers-stephanois.coopcycle.org app_name: Les Coursiers Stéphanois @@ -116,6 +125,7 @@ jobs: name: Build Kooglof app uses: ./.github/workflows/fastlane_android.yml with: + tag: ${{ inputs.tag }} instance: kooglof instance_url: https://kooglof.coopcycle.org app_name: Kooglof @@ -131,6 +141,7 @@ jobs: name: Build RobinFood app uses: ./.github/workflows/fastlane_android.yml with: + tag: ${{ inputs.tag }} instance: robinfood instance_url: https://robinfood.coopcycle.org app_name: Robin Food @@ -146,6 +157,7 @@ jobs: name: Build Coursiers MTP app uses: ./.github/workflows/fastlane_android.yml with: + tag: ${{ inputs.tag }} instance: coursiersmontpellier instance_url: https://coursiersmontpellier.coopcycle.org app_name: Coursiers MTP @@ -161,6 +173,7 @@ jobs: name: Build LCR app uses: ./.github/workflows/fastlane_android.yml with: + tag: ${{ inputs.tag }} instance: lcr instance_url: https://lcr.coopcycle.org app_name: Les Coursiers Rennais diff --git a/.github/workflows/fastlane_android.yml b/.github/workflows/fastlane_android.yml index befb15947..df1725d52 100644 --- a/.github/workflows/fastlane_android.yml +++ b/.github/workflows/fastlane_android.yml @@ -2,6 +2,10 @@ name: Fastlane Android on: workflow_call: inputs: + tag: + type: string + description: Deploy a specific git tag + required: true instance: type: string required: false @@ -33,10 +37,6 @@ on: type: string required: false default: "production" - branch: - type: string - required: false - default: "master" deploy_google_play: type: boolean required: false @@ -48,7 +48,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - ref: ${{ inputs.branch }} + ref: ${{ inputs.tag }} - uses: actions/setup-node@v4 with: node-version: '20.x' @@ -202,7 +202,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - ref: ${{ inputs.branch }} + ref: ${{ inputs.tag }} - name: Download apk artifact uses: actions/download-artifact@v3 with: From 7dc9050ae7b83b9c13fa8a0ebaadd1c84acf0d4f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 31 May 2024 15:23:31 -0700 Subject: [PATCH 40/69] fixed: filter for the `Picked orders` section --- src/redux/Restaurant/selectors.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index 9d6fe771b..8d6923c1c 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -101,6 +101,7 @@ export const selectPickedOrders = createSelector( orders, o => matchesDate(o, date) && + o.state === STATE.READY && o.events.findIndex(ev => ev.type === EVENT.PICKED) !== -1, ), [o => moment.parseZone(o.pickupExpectedAt)], From 35d2710d58f6a08848a9bbb46a351f6835148331 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 31 May 2024 15:28:53 -0700 Subject: [PATCH 41/69] display Cancel and Delay buttons in the In preparation and Ready states --- src/navigation/restaurant/Order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navigation/restaurant/Order.js b/src/navigation/restaurant/Order.js index 471dea151..7fea23185 100644 --- a/src/navigation/restaurant/Order.js +++ b/src/navigation/restaurant/Order.js @@ -87,7 +87,7 @@ class OrderScreen extends Component { } /> )} - {canEdit && order.state === 'accepted' && ( + {canEdit && (order.state === 'accepted' || order.state === 'started' || order.state === 'ready') && ( From 3e7105bb38b6a26cc8bffa738b181f23aed4ae2f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 31 May 2024 17:57:01 -0700 Subject: [PATCH 42/69] display if an order has been printed next to the list item --- src/i18n/locales/en.json | 3 +- .../restaurant/components/OrderListItem.js | 119 ++++++++++++++---- .../components/OrdersToPrintQueue.js | 30 +---- src/redux/Restaurant/reducers.js | 93 +++++++++++--- src/redux/Restaurant/selectors.js | 39 +++++- 5 files changed, 213 insertions(+), 71 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 5a78ec843..17afc086b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -323,7 +323,8 @@ "SCAN_FOR_PRINTERS": "Tap to scan", "SEARCH_WITH_ADDRESS": "Search « {{address}} »", "RESTAURANT_ORDER_CONNECT_PRINTER": "Connect a printer", - "RESTAURANT_ORDER_PRINTING": "Printing order {{number}} (#{{id}})", + "RESTAURANT_ORDER_PRINTING": "Printing order", + "RESTAURANT_ORDER_FAILED_TO_PRINT": "Failed to print", "SWIPE_TO_ACCEPT_REFUSE": "Slide to the right to accept, to the left to refuse", "ADD_COUPON": "Add a voucher code", "VOUCHER_CODE": "Voucher code", diff --git a/src/navigation/restaurant/components/OrderListItem.js b/src/navigation/restaurant/components/OrderListItem.js index f2613d6ea..8783a7901 100644 --- a/src/navigation/restaurant/components/OrderListItem.js +++ b/src/navigation/restaurant/components/OrderListItem.js @@ -1,14 +1,25 @@ -import { StyleSheet, TouchableOpacity, View } from 'react-native'; -import { HStack, Icon, Text } from 'native-base'; +import React from 'react'; +import { + ActivityIndicator, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native'; +import { Icon, Row, Text } from 'native-base'; +import { useTranslation } from 'react-i18next'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import moment from 'moment/moment'; +import Ionicons from 'react-native-vector-icons/Ionicons'; +import { useDispatch, useSelector } from 'react-redux'; import OrderNumber from '../../../components/OrderNumber'; import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon'; import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo'; -import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import { + selectAutoAcceptOrdersEnabled, + selectOrderIdsFailedToPrint, + selectPrintingOrderId, +} from '../../../redux/Restaurant/selectors'; import { formatPrice } from '../../../utils/formatting'; -import moment from 'moment/moment'; -import Ionicons from 'react-native-vector-icons/Ionicons'; -import React from 'react'; -import { useDispatch } from 'react-redux'; import { acceptOrder, finishPreparing, @@ -45,9 +56,64 @@ const styles = StyleSheet.create({ moveForwardIcon: { color: 'white', }, + statusContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 10, + paddingHorizontal: 20, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + printing: { + backgroundColor: '#26de81', + borderBottomColor: '#1cb568', + }, + failedToPrint: { + backgroundColor: '#f7b731', + borderBottomColor: '#eca309', + }, + statusText: { + color: 'white', + textAlign: 'center', + fontSize: 14, + fontWeight: '700', + }, }); +function OrderPrintStatus({ order }) { + const printingOrderId = useSelector(selectPrintingOrderId); + const orderIdsFailedToPrint = useSelector(selectOrderIdsFailedToPrint); + + const isPrinting = order['@id'] === printingOrderId; + const isFailedToPrint = orderIdsFailedToPrint.indexOf(order['@id']) !== -1; + + const { t } = useTranslation(); + + if (isPrinting) { + return ( + + {t('RESTAURANT_ORDER_PRINTING')} + + + ); + } + + if (isFailedToPrint) { + return ( + + + {t('RESTAURANT_ORDER_FAILED_TO_PRINT')} + + + ); + } + + return null; +} + export default function OrderListItem({ order, onItemClick }) { + const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled); + const dispatch = useDispatch(); const isActionable = [STATE.NEW, STATE.ACCEPTED, STATE.STARTED].includes( @@ -56,24 +122,27 @@ export default function OrderListItem({ order, onItemClick }) { return ( - onItemClick(order)}> - - - - - - - {order.notes ? ( - - ) : null} - - {`${formatPrice(order.itemsTotal)}`} - {moment.parseZone(order.pickupExpectedAt).format('LT')} + onItemClick(order)}> + + + + + + + + {order.notes ? ( + + ) : null} + + {`${formatPrice(order.itemsTotal)}`} + {moment.parseZone(order.pickupExpectedAt).format('LT')} + + {autoAcceptOrdersEnabled && order.state === STATE.ACCEPTED ? ( + + ) : null} {isActionable ? ( - selectOrderById(state, printingOrderId), - ); + const { printerConnected } = usePrinter(); const { t } = useTranslation(); @@ -75,18 +65,6 @@ export default function OrdersToPrintQueue() { {t('RESTAURANT_ORDER_CONNECT_PRINTER')} ); - } else if (printingOrder) { - return ( - - - {t('RESTAURANT_ORDER_PRINTING', { - number: printingOrder.number, - id: printingOrder.id, - })} - - - - ); } else { return null; } @@ -105,10 +83,6 @@ const styles = StyleSheet.create({ backgroundColor: '#f7b731', borderBottomColor: '#eca309', }, - printing: { - backgroundColor: '#26de81', - borderBottomColor: '#1cb568', - }, text: { color: 'white', textAlign: 'center', diff --git a/src/redux/Restaurant/reducers.js b/src/redux/Restaurant/reducers.js index 5d24a71bf..a98b66ee3 100644 --- a/src/redux/Restaurant/reducers.js +++ b/src/redux/Restaurant/reducers.js @@ -97,8 +97,20 @@ const initialState = { isSunmiPrinter: false, bluetoothStarted: false, loopeatFormats: {}, - orderIdsToPrint: [], + /** + * { + * [orderId]: { + * copiesToPrint: number, + * failedAttempts: number, + * } + * } + */ + ordersToPrint: {}, printingOrderId: null, + preferences: { + printOrdersNumberOfCopies: 1, + printOrdersMaxFailedAttempts: 3, + }, }; const spliceOrders = (state, payload) => { @@ -181,14 +193,21 @@ const spliceProductOptions = (state, payload) => { function updateOrdersToPrint(state, orderId) { if (state.restaurant.autoAcceptOrdersEnabled) { - if (state.orderIdsToPrint.includes(orderId)) { + if (state.ordersToPrint[orderId]) { return state; - } else { - return { - ...state, - orderIdsToPrint: state.orderIdsToPrint.concat(orderId), - }; } + + return { + ...state, + ordersToPrint: { + ...state.ordersToPrint, + [orderId]: { + copiesToPrint: state.preferences.printOrdersNumberOfCopies, + failedAttempts: 0, + }, + }, + }; + } else { return state; } @@ -531,20 +550,62 @@ export default (state = initialState, action = {}) => { printingOrderId: action.payload['@id'], }; - case printFulfilled.type: - return { - ...state, - orderIdsToPrint: state.orderIdsToPrint.filter( - orderId => orderId !== action.payload['@id'], - ), - printingOrderId: null, - }; + case printFulfilled.type: { + const orderId = action.payload['@id']; + const printTask = state.ordersToPrint[orderId]; + + if (!printTask) { + return state; + } + + if (printTask.copiesToPrint > 1) { + // We have more copies to print + return { + ...state, + printingOrderId: null, + ordersToPrint: { + ...state.ordersToPrint, + [orderId]: { + ...printTask, + copiesToPrint: printTask.copiesToPrint - 1, + failedAttempts: 0, + }, + }, + }; + } else { + // We have printed all needed copies + + const ordersToPrint = { ...state.ordersToPrint }; + delete ordersToPrint[orderId]; + + return { + ...state, + printingOrderId: null, + ordersToPrint: ordersToPrint, + }; + } + } + + case printRejected.type: { + const orderId = action.payload['@id']; + const printTask = state.ordersToPrint[orderId]; + + if (!printTask) { + return state; + } - case printRejected.type: return { ...state, printingOrderId: null, + ordersToPrint: { + ...state.ordersToPrint, + [orderId]: { + ...printTask, + failedAttempts: printTask.failedAttempts + 1, + }, + }, }; + } case BLUETOOTH_STARTED: return { diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index 8d6923c1c..1aab9ff47 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -140,7 +140,44 @@ export const selectIsPrinterConnected = createSelector( (printer, isSunmiPrinter) => Boolean(printer) || isSunmiPrinter, ); -export const selectOrderIdsToPrint = state => state.restaurant.orderIdsToPrint; +const selectOrdersToPrint = state => state.restaurant.ordersToPrint; + +const selectPrintOrdersMaxFailedAttempts = state => + state.restaurant.preferences.printOrdersMaxFailedAttempts; + +export const selectOrderIdsToPrint = createSelector( + selectOrdersToPrint, + selectPrintOrdersMaxFailedAttempts, + (ordersToPrint, printOrdersMaxFailedAttempts) => { + const orderIdsToPrint = []; + + Object.keys(ordersToPrint).forEach(orderId => { + const printTask = ordersToPrint[orderId]; + if (printTask.failedAttempts <= printOrdersMaxFailedAttempts) { + orderIdsToPrint.push(orderId); + } + }); + + return orderIdsToPrint; + }, +); + +export const selectOrderIdsFailedToPrint = createSelector( + selectOrdersToPrint, + selectPrintOrdersMaxFailedAttempts, + (ordersToPrint, printOrdersMaxFailedAttempts) => { + const orderIdsFailedToPrint = []; + + Object.keys(ordersToPrint).forEach(orderId => { + const printTask = ordersToPrint[orderId]; + if (printTask.failedAttempts > printOrdersMaxFailedAttempts) { + orderIdsFailedToPrint.push(orderId); + } + }); + + return orderIdsFailedToPrint; + }, +); export const selectPrintingOrderId = state => state.restaurant.printingOrderId; From f405fd1c19329515ff1efd98358d6e716ea8717c Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:35:49 -0700 Subject: [PATCH 43/69] fixed: moved orders to the Picked column no matter what the state is --- src/redux/Restaurant/selectors.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index 1aab9ff47..3797c0e78 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -56,12 +56,22 @@ export const selectNewOrders = createSelector( ), ); +function isOrderPicked(order) { + return order.events.findIndex(ev => ev.type === EVENT.PICKED) !== -1; +} + export const selectAcceptedOrders = createSelector( selectDate, _selectOrders, (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && o.state === STATE.ACCEPTED), + _.filter( + orders, + o => + matchesDate(o, date) && + o.state === STATE.ACCEPTED && + !isOrderPicked(o), + ), [o => moment.parseZone(o.pickupExpectedAt)], ), ); @@ -71,7 +81,13 @@ export const selectStartedOrders = createSelector( _selectOrders, (date, orders) => _.sortBy( - _.filter(orders, o => matchesDate(o, date) && o.state === STATE.STARTED), + _.filter( + orders, + o => + matchesDate(o, date) && + o.state === STATE.STARTED && + !isOrderPicked(o), + ), [o => moment.parseZone(o.pickupExpectedAt)], ), ); @@ -84,9 +100,7 @@ export const selectReadyOrders = createSelector( _.filter( orders, o => - matchesDate(o, date) && - o.state === STATE.READY && - o.events.findIndex(ev => ev.type === EVENT.PICKED) === -1, + matchesDate(o, date) && o.state === STATE.READY && !isOrderPicked(o), ), [o => moment.parseZone(o.pickupExpectedAt)], ), @@ -101,8 +115,10 @@ export const selectPickedOrders = createSelector( orders, o => matchesDate(o, date) && - o.state === STATE.READY && - o.events.findIndex(ev => ev.type === EVENT.PICKED) !== -1, + (o.state === STATE.ACCEPTED || + o.state === STATE.STARTED || + o.state === STATE.READY) && + isOrderPicked(o), ), [o => moment.parseZone(o.pickupExpectedAt)], ), From b6bfebf92c36c932cba22d861fcd037d96fd17c4 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:01:23 -0700 Subject: [PATCH 44/69] Temporarily display new sections only for restaurants with auto accept orders enabled --- .../restaurant/components/OrderList.js | 36 ++++++++++++------- .../restaurant/components/OrderListItem.js | 6 ++-- src/redux/Restaurant/selectors.js | 31 ++++++++++++++++ 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js index ba4231bb3..0eb4c9108 100644 --- a/src/navigation/restaurant/components/OrderList.js +++ b/src/navigation/restaurant/components/OrderList.js @@ -7,6 +7,8 @@ import { selectAutoAcceptOrdersEnabled, selectCancelledOrders, selectFulfilledOrders, + selectHasReadyState, + selectHasStartedState, selectNewOrders, selectPickedOrders, selectReadyOrders, @@ -18,6 +20,8 @@ import { View } from 'native-base'; export default function OrderList({ onItemClick }) { const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled); + const hasStartedState = useSelector(selectHasStartedState); + const hasReadyState = useSelector(selectHasReadyState); const newOrders = useSelector(selectNewOrders); const acceptedOrders = useSelector(selectAcceptedOrders); @@ -46,18 +50,26 @@ export default function OrderList({ onItemClick }) { }), data: acceptedOrders, }, - { - title: t('RESTAURANT_ORDER_LIST_STARTED_ORDERS', { - count: startedOrders.length, - }), - data: startedOrders, - }, - { - title: t('RESTAURANT_ORDER_LIST_READY_ORDERS', { - count: readyOrders.length, - }), - data: readyOrders, - }, + ...(hasStartedState + ? [ + { + title: t('RESTAURANT_ORDER_LIST_STARTED_ORDERS', { + count: startedOrders.length, + }), + data: startedOrders, + }, + ] + : []), + ...(hasReadyState + ? [ + { + title: t('RESTAURANT_ORDER_LIST_READY_ORDERS', { + count: readyOrders.length, + }), + data: readyOrders, + }, + ] + : []), { title: t('RESTAURANT_ORDER_LIST_PICKED_ORDERS', { count: pickedOrders.length, diff --git a/src/navigation/restaurant/components/OrderListItem.js b/src/navigation/restaurant/components/OrderListItem.js index 8783a7901..3e06a3040 100644 --- a/src/navigation/restaurant/components/OrderListItem.js +++ b/src/navigation/restaurant/components/OrderListItem.js @@ -16,6 +16,7 @@ import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMeth import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo'; import { selectAutoAcceptOrdersEnabled, + selectIsActionable, selectOrderIdsFailedToPrint, selectPrintingOrderId, } from '../../../redux/Restaurant/selectors'; @@ -113,13 +114,10 @@ function OrderPrintStatus({ order }) { export default function OrderListItem({ order, onItemClick }) { const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled); + const isActionable = useSelector(state => selectIsActionable(state, order)); const dispatch = useDispatch(); - const isActionable = [STATE.NEW, STATE.ACCEPTED, STATE.STARTED].includes( - order.state, - ); - return ( onItemClick(order)}> diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index 3797c0e78..887733acd 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -46,6 +46,20 @@ export const selectAutoAcceptOrdersEnabled = createSelector( restaurant => restaurant?.autoAcceptOrdersEnabled ?? false, ); +// Temporarily display new sections only for restaurants with auto accept orders enabled +// Later on: decide when to display these sections to other restaurants +export const selectHasStartedState = createSelector( + selectAutoAcceptOrdersEnabled, + autoAcceptOrdersEnabled => autoAcceptOrdersEnabled, +); + +// Temporarily display new sections only for restaurants with auto accept orders enabled +// Later on: decide when to display these sections to other restaurants +export const selectHasReadyState = createSelector( + selectAutoAcceptOrdersEnabled, + autoAcceptOrdersEnabled => autoAcceptOrdersEnabled, +); + export const selectNewOrders = createSelector( selectDate, _selectOrders, @@ -210,3 +224,20 @@ export const selectOrderById = createSelector( } }, ); + +const selectOrder = (state, order) => order; + +export const selectIsActionable = createSelector( + selectHasStartedState, + selectHasReadyState, + selectOrder, + (hasStartedState, hasReadyState, order) => { + return ( + [ + STATE.NEW, + ...(hasStartedState ? [STATE.ACCEPTED] : []), + ...(hasReadyState ? [STATE.STARTED] : []), + ].includes(order.state) && !isOrderPicked(order) + ); + }, +); From 7e474860d03668a3c4287d167b148eaa3addc770 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:50:59 -0700 Subject: [PATCH 45/69] refactoring: use function component --- src/navigation/restaurant/Printer.js | 161 +++++++++++++++------------ src/redux/Restaurant/reducers.js | 4 +- 2 files changed, 91 insertions(+), 74 deletions(-) diff --git a/src/navigation/restaurant/Printer.js b/src/navigation/restaurant/Printer.js index 2e0b084f8..e89af2428 100644 --- a/src/navigation/restaurant/Printer.js +++ b/src/navigation/restaurant/Printer.js @@ -1,7 +1,8 @@ import _ from 'lodash'; import { Center, Icon, Text } from 'native-base'; import React, { Component } from 'react'; -import { withTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation, withTranslation } from 'react-i18next'; import { ActivityIndicator, FlatList, @@ -14,6 +15,7 @@ import { } from 'react-native'; import FontAwesome from 'react-native-vector-icons/FontAwesome'; import { connect } from 'react-redux'; +import { useNavigation } from '@react-navigation/native'; import ItemSeparator from '../../components/ItemSeparator'; import { @@ -21,11 +23,91 @@ import { connectPrinter, disconnectPrinter, } from '../../redux/Restaurant/actions'; +import { selectPrinter } from '../../redux/Restaurant/selectors'; import { getMissingAndroidPermissions } from '../../utils/bluetooth'; const BleManagerModule = NativeModules.BleManager; const bleManagerEmitter = new NativeEventEmitter(BleManagerModule); +function Item({ item }) { + const dispatch = useDispatch(); + const navigation = useNavigation(); + + const _connect = device => { + dispatch( + connectPrinter(device, () => navigation.navigate('RestaurantSettings')), + ); + }; + + const _disconnect = device => { + dispatch( + disconnectPrinter(device, () => + navigation.navigate('RestaurantSettings'), + ), + ); + }; + + return ( + (item.isConnected ? _disconnect(item) : _connect(item))}> + + {item.name || + (item.advertising && item.advertising.localName) || + item.id} + + + + ); +} + +function PrinterComponent({ devices, isScanning, _onPressScan }) { + const printer = useSelector(selectPrinter); + + const { t } = useTranslation(); + + let items = []; + if (printer) { + items.push({ ...printer, isConnected: true }); + } else { + items = devices.map(device => ({ ...device, isConnected: false })); + } + + const hasItems = !isScanning && items.length > 0; + + if (!hasItems) { + return ( +
+ + + {t('SCAN_FOR_PRINTERS')} + {isScanning && ( + + )} + +
+ ); + } + + return ( + item.id} + renderItem={({ item }) => } + ItemSeparatorComponent={ItemSeparator} + /> + ); +} + class Printer extends Component { constructor(props) { super(props); @@ -51,18 +133,6 @@ class Printer extends Component { this.discoverPeripheral.remove(); } - _connect(device) { - this.props.connectPrinter(device, () => - this.props.navigation.navigate('RestaurantSettings'), - ); - } - - _disconnect(device) { - this.props.disconnectPrinter(device, () => - this.props.navigation.navigate('RestaurantSettings'), - ); - } - async _onPressScan() { if (this.props.isScanning) { return; @@ -89,65 +159,13 @@ class Printer extends Component { } } - renderItem(item) { - return ( - - item.isConnected ? this._disconnect(item) : this._connect(item) - }> - - {item.name || - (item.advertising && item.advertising.localName) || - item.id} - - - - ); - } - + //FIXME; fully migrate to a functional component render() { - const { devices } = this.state; - const { isScanning, printer } = this.props; - - let items = []; - if (printer) { - items.push({ ...printer, isConnected: true }); - } else { - items = devices.map(device => ({ ...device, isConnected: false })); - } - - const hasItems = !isScanning && items.length > 0; - - if (!hasItems) { - return ( -
- this._onPressScan()} - style={{ padding: 15, alignItems: 'center' }}> - - {this.props.t('SCAN_FOR_PRINTERS')} - {isScanning && ( - - )} - -
- ); - } - return ( - item.id} - renderItem={({ item }) => this.renderItem(item)} - ItemSeparatorComponent={ItemSeparator} + this._onPressScan()} /> ); } @@ -169,14 +187,11 @@ const styles = StyleSheet.create({ function mapStateToProps(state) { return { isScanning: state.restaurant.isScanningBluetooth, - printer: state.restaurant.printer, }; } function mapDispatchToProps(dispatch) { return { - connectPrinter: (device, cb) => dispatch(connectPrinter(device, cb)), - disconnectPrinter: (device, cb) => dispatch(disconnectPrinter(device, cb)), bluetoothStartScan: () => dispatch(bluetoothStartScan()), }; } diff --git a/src/redux/Restaurant/reducers.js b/src/redux/Restaurant/reducers.js index a98b66ee3..b5a923017 100644 --- a/src/redux/Restaurant/reducers.js +++ b/src/redux/Restaurant/reducers.js @@ -92,6 +92,9 @@ const initialState = { menus: [], bluetoothEnabled: false, isScanningBluetooth: false, + /** + * Peripheral (react-native-ble-manager) + */ printer: null, productOptions: [], isSunmiPrinter: false, @@ -207,7 +210,6 @@ function updateOrdersToPrint(state, orderId) { }, }, }; - } else { return state; } From 9b7fac3665ce198491daeb01164705a2d808f329 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:14:32 -0700 Subject: [PATCH 46/69] added: configure number of copies printed --- src/i18n/locales/en.json | 1 + src/navigation/restaurant/Printer.js | 55 ++++++++++++++++++++++++---- src/redux/Restaurant/actions.js | 4 ++ src/redux/Restaurant/reducers.js | 30 +++++++++++++-- src/redux/Restaurant/selectors.js | 19 ++++++---- src/redux/reducers.js | 2 +- 6 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 17afc086b..7012dc3b2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -321,6 +321,7 @@ "TOUCH_TO_RELOAD": "Touch to reload", "OFFLINE": "You are offline", "SCAN_FOR_PRINTERS": "Tap to scan", + "AUTO_ACCEPT_ORDERS_PRINT_NUMBER_OF_COPIES": "Number of copies", "SEARCH_WITH_ADDRESS": "Search « {{address}} »", "RESTAURANT_ORDER_CONNECT_PRINTER": "Connect a printer", "RESTAURANT_ORDER_PRINTING": "Printing order", diff --git a/src/navigation/restaurant/Printer.js b/src/navigation/restaurant/Printer.js index e89af2428..33639ee07 100644 --- a/src/navigation/restaurant/Printer.js +++ b/src/navigation/restaurant/Printer.js @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { Center, Icon, Text } from 'native-base'; +import { Center, Icon, Text, View } from 'native-base'; import React, { Component } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useTranslation, withTranslation } from 'react-i18next'; @@ -22,9 +22,16 @@ import { bluetoothStartScan, connectPrinter, disconnectPrinter, + setPrintNumberOfCopies, } from '../../redux/Restaurant/actions'; -import { selectPrinter } from '../../redux/Restaurant/selectors'; +import { + selectAutoAcceptOrdersEnabled, + selectAutoAcceptOrdersPrintNumberOfCopies, + selectPrinter, +} from '../../redux/Restaurant/selectors'; +import { useBackgroundContainerColor } from '../../styles/theme'; import { getMissingAndroidPermissions } from '../../utils/bluetooth'; +import Range from '../checkout/ProductDetails/Range'; const BleManagerModule = NativeModules.BleManager; const bleManagerEmitter = new NativeEventEmitter(BleManagerModule); @@ -66,9 +73,17 @@ function Item({ item }) { function PrinterComponent({ devices, isScanning, _onPressScan }) { const printer = useSelector(selectPrinter); + const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled); + const printNumberOfCopies = useSelector( + selectAutoAcceptOrdersPrintNumberOfCopies, + ); + + const backgroundColor = useBackgroundContainerColor(); const { t } = useTranslation(); + const dispatch = useDispatch(); + let items = []; if (printer) { items.push({ ...printer, isConnected: true }); @@ -99,12 +114,29 @@ function PrinterComponent({ devices, isScanning, _onPressScan }) { } return ( - item.id} - renderItem={({ item }) => } - ItemSeparatorComponent={ItemSeparator} - /> + + item.id} + renderItem={({ item }) => } + ItemSeparatorComponent={ItemSeparator} + /> + {autoAcceptOrdersEnabled ? ( + + {t('AUTO_ACCEPT_ORDERS_PRINT_NUMBER_OF_COPIES')}: + + dispatch(setPrintNumberOfCopies(printNumberOfCopies - 1)) + } + quantity={printNumberOfCopies} + onPressIncrement={() => + dispatch(setPrintNumberOfCopies(printNumberOfCopies + 1)) + } + /> + + ) : null} + ); } @@ -182,6 +214,13 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'space-between', }, + quantityWrapper: { + margin: 20, + paddingHorizontal: 4, + paddingVertical: 20, + flexDirection: 'row', + gap: 16, + }, }); function mapStateToProps(state) { diff --git a/src/redux/Restaurant/actions.js b/src/redux/Restaurant/actions.js index 5c03d13a9..691105fc0 100644 --- a/src/redux/Restaurant/actions.js +++ b/src/redux/Restaurant/actions.js @@ -244,6 +244,10 @@ export const printPending = createAction('PRINT_PENDING'); export const printFulfilled = createAction('PRINT_FULFILLED'); export const printRejected = createAction('PRINT_REJECTED'); +export const setPrintNumberOfCopies = createAction( + 'SET_PRINT_NUMBER_OF_COPIES', +); + /* * Thunk Creators */ diff --git a/src/redux/Restaurant/reducers.js b/src/redux/Restaurant/reducers.js index b5a923017..056909344 100644 --- a/src/redux/Restaurant/reducers.js +++ b/src/redux/Restaurant/reducers.js @@ -65,6 +65,7 @@ import { printFulfilled, printPending, printRejected, + setPrintNumberOfCopies, startPreparing, } from './actions'; @@ -111,8 +112,10 @@ const initialState = { ordersToPrint: {}, printingOrderId: null, preferences: { - printOrdersNumberOfCopies: 1, - printOrdersMaxFailedAttempts: 3, + autoAcceptOrders: { + printNumberOfCopies: 1, + printMaxFailedAttempts: 3, + }, }, }; @@ -200,12 +203,19 @@ function updateOrdersToPrint(state, orderId) { return state; } + const numberOfCopies = + state.preferences.autoAcceptOrders.printNumberOfCopies; + + if (numberOfCopies === 0) { + return state; + } + return { ...state, ordersToPrint: { ...state.ordersToPrint, [orderId]: { - copiesToPrint: state.preferences.printOrdersNumberOfCopies, + copiesToPrint: numberOfCopies, failedAttempts: 0, }, }, @@ -609,6 +619,20 @@ export default (state = initialState, action = {}) => { }; } + case setPrintNumberOfCopies.type: { + const numberOfCopies = action.payload; + return { + ...state, + preferences: { + ...state.preferences, + autoAcceptOrders: { + ...state.preferences.autoAcceptOrders, + printNumberOfCopies: numberOfCopies, + }, + }, + }; + } + case BLUETOOTH_STARTED: return { ...state, diff --git a/src/redux/Restaurant/selectors.js b/src/redux/Restaurant/selectors.js index 887733acd..00205ea3f 100644 --- a/src/redux/Restaurant/selectors.js +++ b/src/redux/Restaurant/selectors.js @@ -172,18 +172,21 @@ export const selectIsPrinterConnected = createSelector( const selectOrdersToPrint = state => state.restaurant.ordersToPrint; -const selectPrintOrdersMaxFailedAttempts = state => - state.restaurant.preferences.printOrdersMaxFailedAttempts; +export const selectAutoAcceptOrdersPrintNumberOfCopies = state => + state.restaurant.preferences.autoAcceptOrders.printNumberOfCopies; + +const selectAutoAcceptOrdersPrintMaxFailedAttempts = state => + state.restaurant.preferences.autoAcceptOrders.printMaxFailedAttempts; export const selectOrderIdsToPrint = createSelector( selectOrdersToPrint, - selectPrintOrdersMaxFailedAttempts, - (ordersToPrint, printOrdersMaxFailedAttempts) => { + selectAutoAcceptOrdersPrintMaxFailedAttempts, + (ordersToPrint, printMaxFailedAttempts) => { const orderIdsToPrint = []; Object.keys(ordersToPrint).forEach(orderId => { const printTask = ordersToPrint[orderId]; - if (printTask.failedAttempts <= printOrdersMaxFailedAttempts) { + if (printTask.failedAttempts <= printMaxFailedAttempts) { orderIdsToPrint.push(orderId); } }); @@ -194,13 +197,13 @@ export const selectOrderIdsToPrint = createSelector( export const selectOrderIdsFailedToPrint = createSelector( selectOrdersToPrint, - selectPrintOrdersMaxFailedAttempts, - (ordersToPrint, printOrdersMaxFailedAttempts) => { + selectAutoAcceptOrdersPrintMaxFailedAttempts, + (ordersToPrint, printMaxFailedAttempts) => { const orderIdsFailedToPrint = []; Object.keys(ordersToPrint).forEach(orderId => { const printTask = ordersToPrint[orderId]; - if (printTask.failedAttempts > printOrdersMaxFailedAttempts) { + if (printTask.failedAttempts > printMaxFailedAttempts) { orderIdsFailedToPrint.push(orderId); } }); diff --git a/src/redux/reducers.js b/src/redux/reducers.js index 003aa1c6d..a6a71e99a 100644 --- a/src/redux/reducers.js +++ b/src/redux/reducers.js @@ -49,7 +49,7 @@ const taskEntitiesPersistConfig = { const restaurantPersistConfig = { key: 'restaurant', storage: AsyncStorage, - whitelist: ['myRestaurants', 'restaurant', 'printer'], + whitelist: ['myRestaurants', 'restaurant', 'printer', 'preferences'], }; const tasksUiPersistConfig = { From b13a9a33decbabfefbdaf11000f4ea8c837d2260 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:30:41 -0700 Subject: [PATCH 47/69] bump version code --- android/app/build.gradle | 4 ++-- ios/CoopCycle/Info.plist | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 03c6642b5..7a4c7dbae 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled true - versionCode 206 - versionName "2.17.2" + versionCode 207 + versionName "2.17.3" manifestPlaceholders = [ tipsiStripeRedirectScheme: "coopcycle", diff --git a/ios/CoopCycle/Info.plist b/ios/CoopCycle/Info.plist index 6473569a6..53c164ae1 100644 --- a/ios/CoopCycle/Info.plist +++ b/ios/CoopCycle/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.17.2 + 2.17.3 CFBundleSignature ???? CFBundleURLTypes @@ -37,7 +37,7 @@ CFBundleVersion - 206 + 207 FacebookAppID 1303550106471840 FacebookClientToken From 1219adac698576842eb72f70b7a02bb07d53ecb7 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:57:06 -0700 Subject: [PATCH 48/69] changed: upload to `internal` track by default --- .github/workflows/build_android.yml | 4 ++-- .github/workflows/fastlane_android.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 584a8e01a..7a913ad75 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -7,7 +7,7 @@ on: description: Deploy a specific git tag required: true deploy_google_play: - description: 'Deploy to Google Play' + description: 'Upload to Google Play' required: true type: boolean default: true @@ -15,7 +15,7 @@ on: description: 'Google Play track' required: true type: string - default: 'production' + default: 'internal' build_official: description: 'Build official app' required: true diff --git a/.github/workflows/fastlane_android.yml b/.github/workflows/fastlane_android.yml index df1725d52..37474ece8 100644 --- a/.github/workflows/fastlane_android.yml +++ b/.github/workflows/fastlane_android.yml @@ -36,7 +36,7 @@ on: google_play_track: type: string required: false - default: "production" + default: "internal" deploy_google_play: type: boolean required: false From e28bb85b69b303e1c1e37b38a3d5208617cb8e8f Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:58:49 -0700 Subject: [PATCH 49/69] updated description --- .github/workflows/fastlane_android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fastlane_android.yml b/.github/workflows/fastlane_android.yml index 37474ece8..336c143d5 100644 --- a/.github/workflows/fastlane_android.yml +++ b/.github/workflows/fastlane_android.yml @@ -4,7 +4,7 @@ on: inputs: tag: type: string - description: Deploy a specific git tag + description: Build a specific git tag required: true instance: type: string From 2e7b3a6441b6e91583132a50987f701d708305b1 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:59:57 -0700 Subject: [PATCH 50/69] updated description --- .github/workflows/build_android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 7a913ad75..495a2400d 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -4,7 +4,7 @@ on: inputs: tag: type: string - description: Deploy a specific git tag + description: Build a specific git tag required: true deploy_google_play: description: 'Upload to Google Play' From 968b942306d0f52971daa271d1b9b01a1d2eefa0 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:17:49 -0700 Subject: [PATCH 51/69] changed: don't upload to Google Play by default --- .github/workflows/build_android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 495a2400d..b2aa8a152 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -10,7 +10,7 @@ on: description: 'Upload to Google Play' required: true type: boolean - default: true + default: false google_play_track: description: 'Google Play track' required: true From e2e32a1f47352fbbb326aa92683873905465e15e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:18:22 -0700 Subject: [PATCH 52/69] changed: allow to build iOS app from a specific tag --- .github/workflows/build_ios.yml | 8 ++++++++ .github/workflows/fastlane_ios.yml | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/.github/workflows/build_ios.yml b/.github/workflows/build_ios.yml index 1a119f8f7..f820967d2 100644 --- a/.github/workflows/build_ios.yml +++ b/.github/workflows/build_ios.yml @@ -2,6 +2,10 @@ name: Build iOS on: workflow_dispatch: inputs: + tag: + type: string + description: Build a specific git tag + required: true build_official: description: 'Build official app' required: true @@ -24,6 +28,7 @@ jobs: name: Build default app uses: ./.github/workflows/fastlane_ios.yml with: + tag: ${{ inputs.tag }} google_service_info_plist_base64: GOOGLE_SERVICE_INFO_PLIST_BASE64 secrets: inherit naofood: @@ -31,6 +36,7 @@ jobs: name: Build Naofood app uses: ./.github/workflows/fastlane_ios.yml with: + tag: ${{ inputs.tag }} instance: naofood instance_url: https://naofood.coopcycle.org app_name: Naofood @@ -43,6 +49,7 @@ jobs: name: Build Kooglof app uses: ./.github/workflows/fastlane_ios.yml with: + tag: ${{ inputs.tag }} instance: kooglof instance_url: https://kooglof.coopcycle.org app_name: Kooglof @@ -55,6 +62,7 @@ jobs: name: Build RobinFood app uses: ./.github/workflows/fastlane_ios.yml with: + tag: ${{ inputs.tag }} instance: robinfood instance_url: https://robinfood.coopcycle.org app_name: Robin Food diff --git a/.github/workflows/fastlane_ios.yml b/.github/workflows/fastlane_ios.yml index 1df1eb173..6a664049c 100644 --- a/.github/workflows/fastlane_ios.yml +++ b/.github/workflows/fastlane_ios.yml @@ -2,6 +2,10 @@ name: Fastlane iOS on: workflow_call: inputs: + tag: + type: string + description: Build a specific git tag + required: true instance: type: string required: false @@ -29,6 +33,8 @@ jobs: runs-on: macOS-14 steps: - uses: actions/checkout@v3 + with: + ref: ${{ inputs.tag }} - uses: actions/setup-node@v4 with: node-version: '20.x' From bf97cb2e5b945cc9f642dc73a663774e289e2f07 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:36:42 -0700 Subject: [PATCH 53/69] updated: command name --- .github/workflows/build_android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index b2aa8a152..b7f566827 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -17,7 +17,7 @@ on: type: string default: 'internal' build_official: - description: 'Build official app' + description: 'Build official production app' required: true type: boolean build_official_beta: From a8fa8f779619e793574b38b04e0d7de00e94471b Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:43:46 -0700 Subject: [PATCH 54/69] updated: command names --- .github/workflows/build_android.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index b7f566827..4b14f444c 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -17,11 +17,11 @@ on: type: string default: 'internal' build_official: - description: 'Build official production app' + description: 'Build CoopCycle production app' required: true type: boolean build_official_beta: - description: 'Build official beta app' + description: 'Build CoopCycle beta app' required: true type: boolean build_coursiers_stephanois: @@ -49,18 +49,18 @@ on: required: true type: boolean jobs: - default: + coopcycle: if: ${{ inputs.build_official }} - name: Build default app + name: Build CoopCycle production app uses: ./.github/workflows/fastlane_android.yml secrets: inherit with: tag: ${{ inputs.tag }} google_play_track: ${{ inputs.google_play_track }} deploy_google_play: ${{ inputs.deploy_google_play }} - default_beta: + coopcycle_beta: if: ${{ inputs.build_official_beta }} - name: Build default beta app + name: Build CoopCycle beta app uses: ./.github/workflows/fastlane_android.yml with: tag: ${{ inputs.tag }} From e65c0a75d3ae54ef86c8d5d95a19b3626cdbbb63 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:51:33 -0700 Subject: [PATCH 55/69] updated: command names --- .github/workflows/build_ios.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build_ios.yml b/.github/workflows/build_ios.yml index f820967d2..d23172e3c 100644 --- a/.github/workflows/build_ios.yml +++ b/.github/workflows/build_ios.yml @@ -7,7 +7,7 @@ on: description: Build a specific git tag required: true build_official: - description: 'Build official app' + description: 'Build CoopCycle production app' required: true type: boolean build_naofood: @@ -23,9 +23,9 @@ on: required: true type: boolean jobs: - default: + coopcycle: if: ${{ inputs.build_official }} - name: Build default app + name: Build CoopCycle production app uses: ./.github/workflows/fastlane_ios.yml with: tag: ${{ inputs.tag }} From 16da72df3e54cbd590030cc4baf99e54a5cb4c8d Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 16:56:39 -0700 Subject: [PATCH 56/69] WIP: iOS beta app setup --- .github/workflows/build_ios.yml | 15 +++++++++++++++ ios/fastlane/metadata-beta/app_icon.png | Bin 0 -> 52461 bytes 2 files changed, 15 insertions(+) create mode 100644 ios/fastlane/metadata-beta/app_icon.png diff --git a/.github/workflows/build_ios.yml b/.github/workflows/build_ios.yml index d23172e3c..44f2ea81a 100644 --- a/.github/workflows/build_ios.yml +++ b/.github/workflows/build_ios.yml @@ -10,6 +10,10 @@ on: description: 'Build CoopCycle production app' required: true type: boolean + build_official_beta: + description: 'Build CoopCycle beta app (TODO: finish setup)' + required: true + type: boolean build_naofood: description: 'Build Naofood app' required: true @@ -31,6 +35,17 @@ jobs: tag: ${{ inputs.tag }} google_service_info_plist_base64: GOOGLE_SERVICE_INFO_PLIST_BASE64 secrets: inherit + coopcycle_beta: + if: ${{ inputs.build_official_beta }} + name: Build CoopCycle beta app + uses: ./.github/workflows/fastlane_ios.yml + with: + tag: ${{ inputs.tag }} + instance: beta + app_name: CoopCycle (Beta) + app_id: org.coopcycle.CoopCycleBeta + google_service_info_plist_base64: GOOGLE_SERVICE_INFO_PLIST_BASE64_BETA + secrets: inherit naofood: if: ${{ inputs.build_naofood }} name: Build Naofood app diff --git a/ios/fastlane/metadata-beta/app_icon.png b/ios/fastlane/metadata-beta/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4f220ced066a60206126e612a5ecd1e66a3b789a GIT binary patch literal 52461 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 Date: Tue, 4 Jun 2024 17:11:03 -0700 Subject: [PATCH 57/69] added: customise build run name --- .github/workflows/build_android.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 4b14f444c..f43353daa 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -1,4 +1,16 @@ name: Build Android +run-name: > + Build Android apps: + {{ github.event.inputs.build_official && 'CoopCycle PROD, ' }} + {{ github.event.inputs.build_official_beta && 'CoopCycle BETA, ' }} + {{ github.event.inputs.build_coursiers_stephanois && 'Les Coursiers Stéphanois, ' }} + {{ github.event.inputs.build_naofood && 'Naofood, ' }} + {{ github.event.inputs.build_zampate && 'Zampate, ' }} + {{ github.event.inputs.build_kooglof && 'Kooglof, ' }} + {{ github.event.inputs.build_robinfood && 'RobinFood, ' }} + {{ github.event.inputs.build_coursiers_rennais && 'Les Coursiers Rennais, ' }} + {{ github.event.inputs.build_coursiers_montpellier && 'Coursiers MTP,' }} + at tag: {{ github.event.inputs.tag }}" on: workflow_dispatch: inputs: From 55d1936b23818755629aa72f9d1faf37cf057050 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:18:03 -0700 Subject: [PATCH 58/69] fixed: customise build run name --- .github/workflows/build_android.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index f43353daa..7cfe3b2c2 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -1,16 +1,16 @@ name: Build Android run-name: > Build Android apps: - {{ github.event.inputs.build_official && 'CoopCycle PROD, ' }} - {{ github.event.inputs.build_official_beta && 'CoopCycle BETA, ' }} - {{ github.event.inputs.build_coursiers_stephanois && 'Les Coursiers Stéphanois, ' }} - {{ github.event.inputs.build_naofood && 'Naofood, ' }} - {{ github.event.inputs.build_zampate && 'Zampate, ' }} - {{ github.event.inputs.build_kooglof && 'Kooglof, ' }} - {{ github.event.inputs.build_robinfood && 'RobinFood, ' }} - {{ github.event.inputs.build_coursiers_rennais && 'Les Coursiers Rennais, ' }} - {{ github.event.inputs.build_coursiers_montpellier && 'Coursiers MTP,' }} - at tag: {{ github.event.inputs.tag }}" + ${{ github.event.inputs.build_official && 'CoopCycle PROD, ' }} + ${{ github.event.inputs.build_official_beta && 'CoopCycle BETA, ' }} + ${{ github.event.inputs.build_coursiers_stephanois && 'Les Coursiers Stéphanois, ' }} + ${{ github.event.inputs.build_naofood && 'Naofood, ' }} + ${{ github.event.inputs.build_zampate && 'Zampate, ' }} + ${{ github.event.inputs.build_kooglof && 'Kooglof, ' }} + ${{ github.event.inputs.build_robinfood && 'RobinFood, ' }} + ${{ github.event.inputs.build_coursiers_rennais && 'Les Coursiers Rennais, ' }} + ${{ github.event.inputs.build_coursiers_montpellier && 'Coursiers MTP,' }} + at tag: ${{ github.event.inputs.tag }} on: workflow_dispatch: inputs: From 3f88840719944ca2aa324a04f56525617abcca7e Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:37:31 -0700 Subject: [PATCH 59/69] WIP: customise build run name --- .github/workflows/build_android.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 7cfe3b2c2..977e0aa8e 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -1,16 +1,15 @@ name: Build Android run-name: > - Build Android apps: - ${{ github.event.inputs.build_official && 'CoopCycle PROD, ' }} - ${{ github.event.inputs.build_official_beta && 'CoopCycle BETA, ' }} - ${{ github.event.inputs.build_coursiers_stephanois && 'Les Coursiers Stéphanois, ' }} - ${{ github.event.inputs.build_naofood && 'Naofood, ' }} - ${{ github.event.inputs.build_zampate && 'Zampate, ' }} - ${{ github.event.inputs.build_kooglof && 'Kooglof, ' }} - ${{ github.event.inputs.build_robinfood && 'RobinFood, ' }} - ${{ github.event.inputs.build_coursiers_rennais && 'Les Coursiers Rennais, ' }} - ${{ github.event.inputs.build_coursiers_montpellier && 'Coursiers MTP,' }} - at tag: ${{ github.event.inputs.tag }} + Build Android apps ${{ inputs.tag }} ${{ inputs.deploy_google_play && format('(and release to {0} track)', inputs.google_play_track) }}: + ${{ inputs.build_official && 'CoopCycle PROD, ' }} + ${{ inputs.build_official_beta && 'CoopCycle BETA, ' }} + ${{ inputs.build_coursiers_stephanois && 'Les Coursiers Stéphanois, ' }} + ${{ inputs.build_naofood && 'Naofood, ' }} + ${{ inputs.build_zampate && 'Zampate, ' }} + ${{ inputs.build_kooglof && 'Kooglof, ' }} + ${{ inputs.build_robinfood && 'RobinFood, ' }} + ${{ inputs.build_coursiers_rennais && 'Les Coursiers Rennais, ' }} + ${{ inputs.build_coursiers_montpellier && 'Coursiers MTP,' }} on: workflow_dispatch: inputs: From 37cfe7ce04ed11e1f4e8d5de4468fb16ecfa022a Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:40:58 -0700 Subject: [PATCH 60/69] fixed: customise build run name --- .github/workflows/build_android.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 977e0aa8e..86ed216ae 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -1,15 +1,15 @@ name: Build Android run-name: > - Build Android apps ${{ inputs.tag }} ${{ inputs.deploy_google_play && format('(and release to {0} track)', inputs.google_play_track) }}: - ${{ inputs.build_official && 'CoopCycle PROD, ' }} - ${{ inputs.build_official_beta && 'CoopCycle BETA, ' }} - ${{ inputs.build_coursiers_stephanois && 'Les Coursiers Stéphanois, ' }} - ${{ inputs.build_naofood && 'Naofood, ' }} - ${{ inputs.build_zampate && 'Zampate, ' }} - ${{ inputs.build_kooglof && 'Kooglof, ' }} - ${{ inputs.build_robinfood && 'RobinFood, ' }} - ${{ inputs.build_coursiers_rennais && 'Les Coursiers Rennais, ' }} - ${{ inputs.build_coursiers_montpellier && 'Coursiers MTP,' }} + Build Android apps ${{ inputs.tag }} ${{ inputs.deploy_google_play && format('(and release to {0} track)', inputs.google_play_track) || '' }}: + ${{ inputs.build_official && 'CoopCycle PROD, ' || '' }} + ${{ inputs.build_official_beta && 'CoopCycle BETA, ' || '' }} + ${{ inputs.build_coursiers_stephanois && 'Les Coursiers Stéphanois, ' || '' }} + ${{ inputs.build_naofood && 'Naofood, ' || '' }} + ${{ inputs.build_zampate && 'Zampate, ' || '' }} + ${{ inputs.build_kooglof && 'Kooglof, ' || '' }} + ${{ inputs.build_robinfood && 'RobinFood, ' || '' }} + ${{ inputs.build_coursiers_rennais && 'Les Coursiers Rennais, ' || '' }} + ${{ inputs.build_coursiers_montpellier && 'Coursiers MTP,' || '' }} on: workflow_dispatch: inputs: From 326369d34f1e61d64120e12985656e06a7d82a8b Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:45:14 -0700 Subject: [PATCH 61/69] fixed: customise build run name --- .github/workflows/build_android.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 86ed216ae..b388c1550 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -1,15 +1,16 @@ name: Build Android run-name: > - Build Android apps ${{ inputs.tag }} ${{ inputs.deploy_google_play && format('(and release to {0} track)', inputs.google_play_track) || '' }}: - ${{ inputs.build_official && 'CoopCycle PROD, ' || '' }} - ${{ inputs.build_official_beta && 'CoopCycle BETA, ' || '' }} - ${{ inputs.build_coursiers_stephanois && 'Les Coursiers Stéphanois, ' || '' }} - ${{ inputs.build_naofood && 'Naofood, ' || '' }} - ${{ inputs.build_zampate && 'Zampate, ' || '' }} - ${{ inputs.build_kooglof && 'Kooglof, ' || '' }} - ${{ inputs.build_robinfood && 'RobinFood, ' || '' }} - ${{ inputs.build_coursiers_rennais && 'Les Coursiers Rennais, ' || '' }} - ${{ inputs.build_coursiers_montpellier && 'Coursiers MTP,' || '' }} + Build Android apps ${{ inputs.tag }} + ${{ inputs.deploy_google_play && format('(and release to {0} track)', inputs.google_play_track) || '' }} + ${{ inputs.build_official && ', CoopCycle PROD' || '' }} + ${{ inputs.build_official_beta && ', CoopCycle BETA' || '' }} + ${{ inputs.build_coursiers_stephanois && ', Les Coursiers Stéphanois' || '' }} + ${{ inputs.build_naofood && ', Naofood' || '' }} + ${{ inputs.build_zampate && ', Zampate' || '' }} + ${{ inputs.build_kooglof && ', Kooglof' || '' }} + ${{ inputs.build_robinfood && ', RobinFood' || '' }} + ${{ inputs.build_coursiers_rennais && ', Les Coursiers Rennais' || '' }} + ${{ inputs.build_coursiers_montpellier && ', Coursiers MTP' || '' }} on: workflow_dispatch: inputs: From 2a29b2567ab16370d309889d0588d0554d159990 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:49:29 -0700 Subject: [PATCH 62/69] fixed: customise build run name --- .github/workflows/build_android.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index b388c1550..5d18c288b 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -1,16 +1,16 @@ name: Build Android run-name: > - Build Android apps ${{ inputs.tag }} + Build ${{ inputs.tag }} ${{ inputs.deploy_google_play && format('(and release to {0} track)', inputs.google_play_track) || '' }} - ${{ inputs.build_official && ', CoopCycle PROD' || '' }} - ${{ inputs.build_official_beta && ', CoopCycle BETA' || '' }} - ${{ inputs.build_coursiers_stephanois && ', Les Coursiers Stéphanois' || '' }} - ${{ inputs.build_naofood && ', Naofood' || '' }} - ${{ inputs.build_zampate && ', Zampate' || '' }} - ${{ inputs.build_kooglof && ', Kooglof' || '' }} - ${{ inputs.build_robinfood && ', RobinFood' || '' }} - ${{ inputs.build_coursiers_rennais && ', Les Coursiers Rennais' || '' }} - ${{ inputs.build_coursiers_montpellier && ', Coursiers MTP' || '' }} + ${{ inputs.build_official && '; CoopCycle PROD' || '' }} + ${{ inputs.build_official_beta && '; CoopCycle BETA' || '' }} + ${{ inputs.build_coursiers_stephanois && '; Les Coursiers Stéphanois' || '' }} + ${{ inputs.build_naofood && '; Naofood' || '' }} + ${{ inputs.build_zampate && '; Zampate' || '' }} + ${{ inputs.build_kooglof && '; Kooglof' || '' }} + ${{ inputs.build_robinfood && '; RobinFood' || '' }} + ${{ inputs.build_coursiers_rennais && '; Les Coursiers Rennais' || '' }} + ${{ inputs.build_coursiers_montpellier && '; Coursiers MTP' || '' }} on: workflow_dispatch: inputs: From 738764b1c2a726581f0fd883de214a1fcf629966 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:54:18 -0700 Subject: [PATCH 63/69] changed: build run name --- .github/workflows/build_android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 5d18c288b..0cfec2def 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -1,7 +1,7 @@ name: Build Android run-name: > - Build ${{ inputs.tag }} - ${{ inputs.deploy_google_play && format('(and release to {0} track)', inputs.google_play_track) || '' }} + ${{ inputs.deploy_google_play && format('Release to {0} track', inputs.google_play_track) || 'Build' }} + ${{ inputs.tag }} ${{ inputs.build_official && '; CoopCycle PROD' || '' }} ${{ inputs.build_official_beta && '; CoopCycle BETA' || '' }} ${{ inputs.build_coursiers_stephanois && '; Les Coursiers Stéphanois' || '' }} From 531fcd32006ae952f88310eff1383eb926c16d68 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:58:30 -0700 Subject: [PATCH 64/69] changed: build run name --- .github/workflows/build_android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 0cfec2def..807ae780d 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -2,8 +2,8 @@ name: Build Android run-name: > ${{ inputs.deploy_google_play && format('Release to {0} track', inputs.google_play_track) || 'Build' }} ${{ inputs.tag }} - ${{ inputs.build_official && '; CoopCycle PROD' || '' }} - ${{ inputs.build_official_beta && '; CoopCycle BETA' || '' }} + ${{ inputs.build_official && '; CoopCycle' || '' }} + ${{ inputs.build_official_beta && '; CoopCycle (Beta)' || '' }} ${{ inputs.build_coursiers_stephanois && '; Les Coursiers Stéphanois' || '' }} ${{ inputs.build_naofood && '; Naofood' || '' }} ${{ inputs.build_zampate && '; Zampate' || '' }} From e0578ea263d6438dd3da64e9cbbe1a76214c6ee6 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:01:19 -0700 Subject: [PATCH 65/69] added: customise build run name --- .github/workflows/build_ios.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build_ios.yml b/.github/workflows/build_ios.yml index 44f2ea81a..37ebd3da6 100644 --- a/.github/workflows/build_ios.yml +++ b/.github/workflows/build_ios.yml @@ -1,4 +1,11 @@ name: Build iOS +run-name: > + Upload to App Store ${{ inputs.tag }} + ${{ inputs.build_official && '; CoopCycle' || '' }} + ${{ inputs.build_official_beta && '; CoopCycle (Beta)' || '' }} + ${{ inputs.build_naofood && '; Naofood' || '' }} + ${{ inputs.build_kooglof && '; Kooglof' || '' }} + ${{ inputs.build_robinfood && '; RobinFood' || '' }} on: workflow_dispatch: inputs: From 5fb4ede915df7b88f2416f2e5c2fb8acbf341c21 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:31:49 -0700 Subject: [PATCH 66/69] put back 'redux-actions's `createAction` for events using legacy style --- src/redux/Restaurant/actions.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/redux/Restaurant/actions.js b/src/redux/Restaurant/actions.js index 691105fc0..25c9b7d03 100644 --- a/src/redux/Restaurant/actions.js +++ b/src/redux/Restaurant/actions.js @@ -117,22 +117,22 @@ export const UPDATE_LOOPEAT_FORMATS_SUCCESS = * Action Creators */ -const loadMyRestaurantsRequest = createAction(LOAD_MY_RESTAURANTS_REQUEST); -const loadMyRestaurantsSuccess = createAction(LOAD_MY_RESTAURANTS_SUCCESS); -const loadMyRestaurantsFailure = createAction(LOAD_MY_RESTAURANTS_FAILURE); - -export const loadOrdersRequest = createAction(LOAD_ORDERS_REQUEST); -export const loadOrdersSuccess = createAction(LOAD_ORDERS_SUCCESS); -export const loadOrdersFailure = createAction(LOAD_ORDERS_FAILURE); - -export const loadOrderRequest = createAction(LOAD_ORDER_REQUEST); -export const loadOrderSuccess = createAction(LOAD_ORDER_SUCCESS); -export const loadOrderFailure = createAction(LOAD_ORDER_FAILURE); - -export const loadMenusRequest = createAction(LOAD_MENUS_REQUEST); -export const loadMenusSuccess = createAction(LOAD_MENUS_SUCCESS); -export const loadMenusFailure = createAction(LOAD_MENUS_FAILURE); -export const setCurrentMenu = createAction( +const loadMyRestaurantsRequest = createFsAction(LOAD_MY_RESTAURANTS_REQUEST); +const loadMyRestaurantsSuccess = createFsAction(LOAD_MY_RESTAURANTS_SUCCESS); +const loadMyRestaurantsFailure = createFsAction(LOAD_MY_RESTAURANTS_FAILURE); + +export const loadOrdersRequest = createFsAction(LOAD_ORDERS_REQUEST); +export const loadOrdersSuccess = createFsAction(LOAD_ORDERS_SUCCESS); +export const loadOrdersFailure = createFsAction(LOAD_ORDERS_FAILURE); + +export const loadOrderRequest = createFsAction(LOAD_ORDER_REQUEST); +export const loadOrderSuccess = createFsAction(LOAD_ORDER_SUCCESS); +export const loadOrderFailure = createFsAction(LOAD_ORDER_FAILURE); + +export const loadMenusRequest = createFsAction(LOAD_MENUS_REQUEST); +export const loadMenusSuccess = createFsAction(LOAD_MENUS_SUCCESS); +export const loadMenusFailure = createFsAction(LOAD_MENUS_FAILURE); +export const setCurrentMenu = createFsAction( SET_CURRENT_MENU, (restaurant, menu) => ({ restaurant, menu }), ); From 931f9a2261ef20f527f0591558be19a00802b8f8 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:29:58 -0700 Subject: [PATCH 67/69] added: update task list on the foreground notification --- src/redux/Courier/taskActions.js | 13 +++++++++++-- .../PushNotificationMiddleware/index.js | 15 +++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/redux/Courier/taskActions.js b/src/redux/Courier/taskActions.js index 76c1428ad..e6e45d1f2 100644 --- a/src/redux/Courier/taskActions.js +++ b/src/redux/Courier/taskActions.js @@ -175,7 +175,7 @@ export function navigateAndLoadTasks(selectedDate) { }; } -export function loadTasks(selectedDate, refresh = false) { +export function loadTasks(selectedDate, refresh = false, cb) { return function (dispatch, getState) { const { httpClient } = getState().app; @@ -205,8 +205,17 @@ export function loadTasks(selectedDate, refresh = false) { ), ); } + + if (cb && typeof cb === 'function') { + setTimeout(() => cb(), 0); + } }) - .catch(e => dispatch(loadTasksFailure(e))); + .catch(e => { + dispatch(loadTasksFailure(e)); + if (cb && typeof cb === 'function') { + setTimeout(() => cb(), 0); + } + }); }; } diff --git a/src/redux/middlewares/PushNotificationMiddleware/index.js b/src/redux/middlewares/PushNotificationMiddleware/index.js index 074c6013d..db945bcc5 100644 --- a/src/redux/middlewares/PushNotificationMiddleware/index.js +++ b/src/redux/middlewares/PushNotificationMiddleware/index.js @@ -18,7 +18,7 @@ import tracker from '../../../analytics/Tracker'; import analyticsEvent from '../../../analytics/Event'; import { loadOrder, loadOrderAndNavigate } from '../../Restaurant/actions'; import { EVENT as EVENT_TASK_COLLECTION } from '../../../domain/TaskCollection'; -import { navigateAndLoadTasks } from '../../Courier/taskActions'; +import { loadTasks, navigateAndLoadTasks } from '../../Courier/taskActions'; // As remote push notifications are configured very early, // most of the time the user won't be authenticated @@ -97,13 +97,20 @@ export default ({ getState, dispatch }) => { }), ); break; - case EVENT_TASK_COLLECTION.CHANGED: + case EVENT_TASK_COLLECTION.CHANGED: { + const dateStr = event.data.date; + dispatch( - foregroundPushNotification(event.name, { - date: event.data.date, + loadTasks(moment(dateStr), true, () => { + dispatch( + foregroundPushNotification(event.name, { + date: dateStr, + }), + ); }), ); break; + } default: break; } From 0f3ab4a82247a007c4d4d0ea87290054dad13736 Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:46:55 -0700 Subject: [PATCH 68/69] removed `loadTasks` request --- src/components/NotificationModal.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/NotificationModal.js b/src/components/NotificationModal.js index 42bf44dcc..160d40feb 100644 --- a/src/components/NotificationModal.js +++ b/src/components/NotificationModal.js @@ -10,7 +10,6 @@ import FontAwesome from 'react-native-vector-icons/FontAwesome'; import NavigationHolder from '../NavigationHolder'; import { CommonActions } from '@react-navigation/native'; import { useDispatch } from 'react-redux'; -import { loadTasks } from '../redux/Courier'; import { EVENT as EVENT_ORDER } from '../domain/Order'; import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; @@ -77,7 +76,7 @@ export default function NotificationModal({ notifications, onDismiss }) { ); }; - const _navigateToTasks = date => { + const _navigateToTasks = () => { onDismiss(); NavigationHolder.dispatch( @@ -91,8 +90,6 @@ export default function NotificationModal({ notifications, onDismiss }) { }, }), ); - - dispatch(loadTasks(moment(date))); }; const renderOrderCreated = order => { From 6551483d7413979ed5bec915de07d1936d43715d Mon Sep 17 00:00:00 2001 From: Vladimir <70273239+vladimir-8@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:53:21 -0700 Subject: [PATCH 69/69] Revert "removed `loadTasks` request" This reverts commit 0f3ab4a82247a007c4d4d0ea87290054dad13736. --- src/components/NotificationModal.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/NotificationModal.js b/src/components/NotificationModal.js index 160d40feb..42bf44dcc 100644 --- a/src/components/NotificationModal.js +++ b/src/components/NotificationModal.js @@ -10,6 +10,7 @@ import FontAwesome from 'react-native-vector-icons/FontAwesome'; import NavigationHolder from '../NavigationHolder'; import { CommonActions } from '@react-navigation/native'; import { useDispatch } from 'react-redux'; +import { loadTasks } from '../redux/Courier'; import { EVENT as EVENT_ORDER } from '../domain/Order'; import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection'; @@ -76,7 +77,7 @@ export default function NotificationModal({ notifications, onDismiss }) { ); }; - const _navigateToTasks = () => { + const _navigateToTasks = date => { onDismiss(); NavigationHolder.dispatch( @@ -90,6 +91,8 @@ export default function NotificationModal({ notifications, onDismiss }) { }, }), ); + + dispatch(loadTasks(moment(date))); }; const renderOrderCreated = order => {