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/.github/workflows/build_android.yml b/.github/workflows/build_android.yml
index 41714599a..40b42142b 100644
--- a/.github/workflows/build_android.yml
+++ b/.github/workflows/build_android.yml
@@ -1,23 +1,37 @@
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' || '' }}
+ ${{ inputs.build_official_beta && '; CoopCycle (Beta)' || '' }}
+ ${{ inputs.build_naofood && '; Naofood' || '' }}
+ ${{ inputs.build_kooglof && '; Kooglof' || '' }}
+ ${{ inputs.build_robinfood && '; RobinFood' || '' }}
+ ${{ inputs.build_coursiers_rennais && '; Les Coursiers Rennais' || '' }}
+ ${{ inputs.build_eraman && '; Eraman' || '' }}
on:
workflow_dispatch:
inputs:
+ tag:
+ type: string
+ description: Build 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
+ default: false
google_play_track:
description: 'Google Play track'
required: true
type: string
- default: 'production'
+ default: 'internal'
build_official:
- description: 'Build official 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_naofood:
@@ -41,19 +55,21 @@ 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 }}
instance: beta
app_name: CoopCycle (Beta)
package_name: fr.coopcycle.beta
@@ -67,6 +83,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
@@ -82,6 +99,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
@@ -97,6 +115,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
@@ -112,6 +131,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
@@ -127,6 +147,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
@@ -142,6 +163,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/build_ios.yml b/.github/workflows/build_ios.yml
index c1a8c0923..7095be2e5 100644
--- a/.github/workflows/build_ios.yml
+++ b/.github/workflows/build_ios.yml
@@ -1,9 +1,24 @@
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:
+ tag:
+ type: string
+ description: Build a specific git tag
+ required: true
build_official:
- description: 'Build official app'
+ 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:
@@ -23,18 +38,31 @@ 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 }}
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
uses: ./.github/workflows/fastlane_ios.yml
with:
+ tag: ${{ inputs.tag }}
instance: naofood
instance_url: https://naofood.coopcycle.org
app_name: Naofood
@@ -47,6 +75,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
@@ -59,6 +88,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_android.yml b/.github/workflows/fastlane_android.yml
index befb15947..336c143d5 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: Build a specific git tag
+ required: true
instance:
type: string
required: false
@@ -32,11 +36,7 @@ on:
google_play_track:
type: string
required: false
- default: "production"
- branch:
- type: string
- required: false
- default: "master"
+ default: "internal"
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:
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'
diff --git a/ios/fastlane/metadata-beta/app_icon.png b/ios/fastlane/metadata-beta/app_icon.png
new file mode 100644
index 000000000..4f220ced0
Binary files /dev/null and b/ios/fastlane/metadata-beta/app_icon.png differ
diff --git a/package.json b/package.json
index e9d899d52..48b9137bc 100644
--- a/package.json
+++ b/package.json
@@ -112,6 +112,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/components/NotificationHandler.js b/src/components/NotificationHandler.js
index 2960b69d4..743870987 100644
--- a/src/components/NotificationHandler.js
+++ b/src/components/NotificationHandler.js
@@ -1,383 +1,64 @@
-import { CommonActions } from '@react-navigation/native';
-import moment from 'moment';
-import { Icon, Text } from 'native-base';
-import React, { Component } from 'react';
-import { withTranslation } from 'react-i18next';
-import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native';
-import Modal from 'react-native-modal';
-import Sound from 'react-native-sound';
-import FontAwesome from 'react-native-vector-icons/FontAwesome';
-import Ionicons from 'react-native-vector-icons/Ionicons';
-import { connect } from 'react-redux';
+import React, { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
-import NavigationHolder from '../NavigationHolder';
-import PushNotification from '../notifications';
-
-import analyticsEvent from '../analytics/Event';
-import tracker from '../analytics/Tracker';
import {
clearNotifications,
- pushNotification,
- registerPushNotificationToken,
+ startSound,
+ stopSound,
} from '../redux/App/actions';
-import { loadTasks, selectTasksChangedAlertSound } from '../redux/Courier';
+import NotificationModal from './NotificationModal';
import {
- loadOrder,
- loadOrderAndNavigate,
- loadOrderAndPushNotification,
-} from '../redux/Restaurant/actions';
-import { message as wsMessage } from '../redux/middlewares/CentrifugoMiddleware/actions';
-
-import ModalContent from './ModalContent';
+ selectNotificationsToDisplay,
+ selectNotificationsWithSound,
+} from '../redux/App/selectors';
+import { AppState } from 'react-native';
-// Make sure sound will play even when device is in silent mode
-Sound.setCategory('Playback');
+const NOTIFICATION_DURATION_MS = 10000;
/**
* This component is used
- * 1/ To configure push notifications (see componentDidMount)
- * 2/ To show notifications when the app is in foreground.
+ * 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
*/
-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() {
+ const notificationsToDisplay = useSelector(selectNotificationsToDisplay);
+ const notificationsWithSound = useSelector(selectNotificationsWithSound);
+
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (
+ notificationsToDisplay.length > 0 ||
+ notificationsWithSound.length > 0
+ ) {
+ setTimeout(() => {
+ dispatch(clearNotifications());
+ }, NOTIFICATION_DURATION_MS);
}
-
- this.props.loadTasks(moment(date));
- }
-
- _loadSound() {
- const bell = new Sound(
- 'misstickle__indian_bell_chime.wav',
- Sound.MAIN_BUNDLE,
- error => {
- if (error) {
- return;
- }
-
- bell.setNumberOfLoops(-1);
-
- this.setState({
- sound: bell,
- isSoundReady: true,
- });
- },
- );
- }
-
- _startSound() {
- const { sound, isSoundReady } = this.state;
- if (isSoundReady) {
- sound.play(success => {
- if (!success) {
- sound.reset();
- }
- });
- // Clear notifications after 10 seconds
- setTimeout(() => this.props.clearNotifications(), 10000);
- }
- }
-
- _stopSound() {
- const { sound, isSoundReady } = this.state;
- if (isSoundReady) {
- sound.stop(() => {});
+ }, [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());
}
- }
-
- 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()}`;
- }
- }
-
- 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()}
-
- );
- }
-}
-
-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),
- };
+ }, [notificationsWithSound, dispatch]);
+
+ return (
+ {
+ dispatch(clearNotifications());
+ }}
+ />
+ );
}
-
-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..42bf44dcc
--- /dev/null
+++ b/src/components/NotificationModal.js
@@ -0,0 +1,175 @@
+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';
+import { EVENT as EVENT_ORDER } from '../domain/Order';
+import { EVENT as EVENT_TASK_COLLECTION } from '../domain/TaskCollection';
+
+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 EVENT_ORDER.CREATED:
+ return `order:created:${item.params.order.id}`;
+ case EVENT_TASK_COLLECTION.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 EVENT_ORDER.CREATED:
+ return renderOrderCreated(notification.params.order);
+ case EVENT_TASK_COLLECTION.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/domain/Order.js b/src/domain/Order.js
new file mode 100644
index 000000000..401d4759c
--- /dev/null
+++ b/src/domain/Order.js
@@ -0,0 +1,16 @@
+export const STATE = {
+ NEW: 'new',
+ ACCEPTED: 'accepted',
+ REFUSED: 'refused',
+ STARTED: 'started',
+ READY: 'ready',
+ FULFILLED: 'fulfilled',
+ CANCELLED: 'cancelled',
+};
+
+export const EVENT = {
+ STATE_CHANGED: 'order:state_changed',
+ CREATED: 'order:created',
+ ACCEPTED: 'order:accepted',
+ PICKED: 'order:picked',
+};
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',
+};
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index de30e33fb..7012dc3b2 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -147,9 +147,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",
@@ -319,8 +321,11 @@
"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",
+ "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/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})`}
diff --git a/src/navigation/restaurant/Dashboard.js b/src/navigation/restaurant/Dashboard.js
index a62560b62..49d34053a 100644
--- a/src/navigation/restaurant/Dashboard.js
+++ b/src/navigation/restaurant/Dashboard.js
@@ -1,10 +1,10 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { Alert, NativeModules } from 'react-native';
+import { Center, VStack } from 'native-base';
+import { useDispatch, useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
import moment from 'moment';
-import { Center, VStack } from 'native-base';
-import React, { Component } from 'react';
-import { withTranslation } from 'react-i18next';
-import { Alert, InteractionManager, NativeModules } from 'react-native';
-import { connect } from 'react-redux';
import DangerAlert from '../../components/DangerAlert';
import Offline from '../../components/Offline';
@@ -13,7 +13,6 @@ import DatePickerHeader from './components/DatePickerHeader';
import OrderList from './components/OrderList';
import WebSocketIndicator from './components/WebSocketIndicator';
-import PushNotification from '../../notifications';
import {
selectIsCentrifugoConnected,
selectIsLoading,
@@ -26,38 +25,103 @@ import {
loadOrders,
} from '../../redux/Restaurant/actions';
import {
- selectAcceptedOrders,
- selectCancelledOrders,
- selectFulfilledOrders,
- selectNewOrders,
- selectPickedOrders,
+ selectDate,
+ selectRestaurant,
selectSpecialOpeningHoursSpecificationForToday,
} from '../../redux/Restaurant/selectors';
+import PushNotification from '../../notifications';
+import OrdersToPrintQueue from './components/OrdersToPrintQueue';
import { connect as connectCentrifugo } from '../../redux/middlewares/CentrifugoMiddleware/actions';
const RNSound = NativeModules.RNSound;
-class DashboardPage extends Component {
- constructor(props) {
- super(props);
+export default function DashboardPage({ navigation, route }) {
+ const restaurant = useSelector(selectRestaurant);
+ const date = useSelector(selectDate);
+ 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();
- this.state = {
- wasAlertShown: false,
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ activateKeepAwakeAsync();
+
+ return () => {
+ deactivateKeepAwake();
};
- }
+ }, []);
- _checkSystemVolume() {
+ 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]);
+
+ // 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) {
+ setWasAlertShown(true);
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 });
-
// 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
@@ -68,188 +132,61 @@ class DashboardPage extends Component {
},
},
{
- text: this.props.t('CANCEL'),
+ text: t('CANCEL'),
style: 'cancel',
- onPress: () => this.setState({ wasAlertShown: 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/Order.js b/src/navigation/restaurant/Order.js
index 7a7b22367..7fea23185 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) {
@@ -83,7 +87,7 @@ class OrderScreen extends Component {
}
/>
)}
- {canEdit && order.state === 'accepted' && (
+ {canEdit && (order.state === 'accepted' || order.state === 'started' || order.state === 'ready') && (
@@ -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/Printer.js b/src/navigation/restaurant/Printer.js
index 2e0b084f8..33639ee07 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 { Center, Icon, Text, View } 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,18 +15,131 @@ 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 {
bluetoothStartScan,
connectPrinter,
disconnectPrinter,
+ setPrintNumberOfCopies,
} from '../../redux/Restaurant/actions';
+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);
+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 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 });
+ } 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}
+ />
+ {autoAcceptOrdersEnabled ? (
+
+ {t('AUTO_ACCEPT_ORDERS_PRINT_NUMBER_OF_COPIES')}:
+
+ dispatch(setPrintNumberOfCopies(printNumberOfCopies - 1))
+ }
+ quantity={printNumberOfCopies}
+ onPressIncrement={() =>
+ dispatch(setPrintNumberOfCopies(printNumberOfCopies + 1))
+ }
+ />
+
+ ) : null}
+
+ );
+}
+
class Printer extends Component {
constructor(props) {
super(props);
@@ -51,18 +165,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 +191,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()}
/>
);
}
@@ -164,19 +214,23 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
},
+ quantityWrapper: {
+ margin: 20,
+ paddingHorizontal: 4,
+ paddingVertical: 20,
+ flexDirection: 'row',
+ gap: 16,
+ },
});
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/navigation/restaurant/components/OrderList.js b/src/navigation/restaurant/components/OrderList.js
index cfb5e5643..0eb4c9108 100644
--- a/src/navigation/restaurant/components/OrderList.js
+++ b/src/navigation/restaurant/components/OrderList.js
@@ -1,116 +1,106 @@
-import moment from 'moment';
-import { HStack, Icon, Text } from 'native-base';
-import React, { Component } from 'react';
-import { withTranslation } from 'react-i18next';
-import { SectionList, StyleSheet, TouchableOpacity, View } from 'react-native';
-import FontAwesome from 'react-native-vector-icons/FontAwesome';
-import Ionicons from 'react-native-vector-icons/Ionicons';
+import React from 'react';
+import { SectionList } from 'react-native';
+import { useTranslation } from 'react-i18next';
+import { useSelector } from 'react-redux';
+import {
+ selectAcceptedOrders,
+ selectAutoAcceptOrdersEnabled,
+ selectCancelledOrders,
+ selectFulfilledOrders,
+ selectHasReadyState,
+ selectHasStartedState,
+ selectNewOrders,
+ selectPickedOrders,
+ selectReadyOrders,
+ selectStartedOrders,
+} from '../../../redux/Restaurant/selectors';
+import OrderListItem from './OrderListItem';
+import OrderListSectionHeader from './OrderListSectionHeader';
+import { View } from 'native-base';
-import ItemSeparatorComponent from '../../../components/ItemSeparator';
-import OrderFulfillmentMethodIcon from '../../../components/OrderFulfillmentMethodIcon';
-import OrderNumber from '../../../components/OrderNumber';
-import { PaymentMethodInfo } from '../../../components/PaymentMethodInfo';
-import { formatPrice } from '../../../utils/formatting';
+export default function OrderList({ onItemClick }) {
+ const autoAcceptOrdersEnabled = useSelector(selectAutoAcceptOrdersEnabled);
+ const hasStartedState = useSelector(selectHasStartedState);
+ const hasReadyState = useSelector(selectHasReadyState);
-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,
- },
-});
+ 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);
-class OrderList extends Component {
- renderItem(order) {
- return (
- this.props.onItemClick(order)}>
-
-
-
-
-
-
- {order.notes ? (
-
- ) : null}
-
- {`${formatPrice(order.itemsTotal)}`}
- {moment.parseZone(order.pickupExpectedAt).format('LT')}
-
-
- );
- }
+ const { t } = useTranslation();
- render() {
- const allOrders = [
- ...this.props.newOrders,
- ...this.props.acceptedOrders,
- ...this.props.pickedOrders,
- ...this.props.cancelledOrders,
- ...this.props.fulfilledOrders,
- ];
-
- return (
- item['@id']}
- sections={[
+ const sections = [
+ ...(autoAcceptOrdersEnabled
+ ? []
+ : [
{
- title: this.props.t('RESTAURANT_ORDER_LIST_NEW_ORDERS', {
- count: this.props.newOrders.length,
+ title: t('RESTAURANT_ORDER_LIST_NEW_ORDERS', {
+ count: newOrders.length,
}),
- data: this.props.newOrders,
+ data: newOrders,
},
+ ]),
+ {
+ title: t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', {
+ count: acceptedOrders.length,
+ }),
+ data: acceptedOrders,
+ },
+ ...(hasStartedState
+ ? [
{
- title: this.props.t('RESTAURANT_ORDER_LIST_ACCEPTED_ORDERS', {
- count: this.props.acceptedOrders.length,
+ title: t('RESTAURANT_ORDER_LIST_STARTED_ORDERS', {
+ count: startedOrders.length,
}),
- data: this.props.acceptedOrders,
+ data: startedOrders,
},
+ ]
+ : []),
+ ...(hasReadyState
+ ? [
{
- title: this.props.t('RESTAURANT_ORDER_LIST_PICKED_ORDERS', {
- count: this.props.pickedOrders.length,
+ title: t('RESTAURANT_ORDER_LIST_READY_ORDERS', {
+ count: readyOrders.length,
}),
- data: this.props.pickedOrders,
+ data: readyOrders,
},
- {
- 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}
- />
- );
- }
-}
+ ]
+ : []),
+ {
+ 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,
+ },
+ ];
-export default withTranslation()(OrderList);
+ return (
+ item['@id']}
+ sections={sections}
+ renderSectionHeader={({ section: { title } }) => (
+
+ )}
+ renderItem={({ item }) => (
+
+ )}
+ renderSectionFooter={() => }
+ />
+ );
+}
diff --git a/src/navigation/restaurant/components/OrderListItem.js b/src/navigation/restaurant/components/OrderListItem.js
new file mode 100644
index 000000000..3e06a3040
--- /dev/null
+++ b/src/navigation/restaurant/components/OrderListItem.js
@@ -0,0 +1,173 @@
+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 {
+ selectAutoAcceptOrdersEnabled,
+ selectIsActionable,
+ selectOrderIdsFailedToPrint,
+ selectPrintingOrderId,
+} from '../../../redux/Restaurant/selectors';
+import { formatPrice } from '../../../utils/formatting';
+import {
+ acceptOrder,
+ finishPreparing,
+ startPreparing,
+} from '../../../redux/Restaurant/actions';
+import { STATE } from '../../../domain/Order';
+
+const styles = StyleSheet.create({
+ item: {
+ flex: 1,
+ flexDirection: 'row',
+ marginVertical: 6,
+ marginLeft: 24,
+ marginRight: 24,
+ borderColor: '#E3E3E3',
+ borderWidth: 1,
+ borderRadius: 4,
+ },
+ content: {
+ padding: 12,
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ number: {
+ marginRight: 10,
+ },
+ moveForward: {
+ backgroundColor: '#5EBE68',
+ paddingHorizontal: 12,
+ justifyContent: 'center',
+ },
+ 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 isActionable = useSelector(state => selectIsActionable(state, order));
+
+ const dispatch = useDispatch();
+
+ return (
+
+ onItemClick(order)}>
+
+
+
+
+
+
+
+ {order.notes ? (
+
+ ) : null}
+
+ {`${formatPrice(order.itemsTotal)}`}
+ {moment.parseZone(order.pickupExpectedAt).format('LT')}
+
+ {autoAcceptOrdersEnabled && order.state === STATE.ACCEPTED ? (
+
+ ) : null}
+
+ {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/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}
+
+ );
+}
diff --git a/src/navigation/restaurant/components/OrdersToPrintQueue.js b/src/navigation/restaurant/components/OrdersToPrintQueue.js
new file mode 100644
index 000000000..b5cebaa3e
--- /dev/null
+++ b/src/navigation/restaurant/components/OrdersToPrintQueue.js
@@ -0,0 +1,92 @@
+import React, { useEffect } from 'react';
+import { StyleSheet, TouchableOpacity } from 'react-native';
+import { Text } from 'native-base';
+import { useTranslation } from 'react-i18next';
+import { useDispatch, useSelector } from 'react-redux';
+import {
+ selectAutoAcceptOrdersEnabled,
+ selectIsPrinterConnected,
+ 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 } = usePrinter();
+
+ const { t } = useTranslation();
+
+ const navigation = useNavigation();
+
+ if (autoAcceptOrdersEnabled && !printerConnected) {
+ return (
+ {
+ navigation.navigate('RestaurantSettings', {
+ screen: 'RestaurantPrinter',
+ });
+ }}>
+ {t('RESTAURANT_ORDER_CONNECT_PRINTER')}
+
+ );
+ } 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',
+ },
+ text: {
+ color: 'white',
+ textAlign: 'center',
+ fontSize: 14,
+ fontWeight: '700',
+ },
+});
diff --git a/src/notifications/index.android.js b/src/notifications/index.android.js
index 31afcdce0..5f8d353c5 100644
--- a/src/notifications/index.android.js
+++ b/src/notifications/index.android.js
@@ -67,7 +67,6 @@ class PushNotification {
messaging()
.getToken()
.then(fcmToken => {
- console.log(fcmToken);
options.onRegister(fcmToken);
});
}
diff --git a/src/redux/App/actions.js b/src/redux/App/actions.js
index 4d03c52be..d5de38db9 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';
@@ -43,8 +44,12 @@ export const DELETE_PUSH_NOTIFICATION_TOKEN_SUCCESS =
export const LOGIN = '@app/LOGIN';
export const SET_LOADING = '@app/SET_LOADING';
-export const PUSH_NOTIFICATION = '@app/PUSH_NOTIFICATION';
+
+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';
@@ -109,100 +114,109 @@ export const SET_INCIDENT_ENABLED = '@app/SET_IS_INCIDENT_ENABLED';
* Action Creators
*/
-export const setLoading = createAction(SET_LOADING);
-export const pushNotification = createAction(
- PUSH_NOTIFICATION,
+export const setLoading = createFsAction(SET_LOADING);
+
+export const foregroundPushNotification = createFsAction(
+ FOREGROUND_PUSH_NOTIFICATION,
(event, params = {}) => ({ event, params }),
);
-export const clearNotifications = createAction(CLEAR_NOTIFICATIONS);
-export const _authenticationRequest = createAction(AUTHENTICATION_REQUEST);
-export const _authenticationSuccess = createAction(AUTHENTICATION_SUCCESS);
-const _authenticationFailure = createAction(AUTHENTICATION_FAILURE);
+export const addNotification = createFsAction(
+ ADD_NOTIFICATION,
+ (event, params = {}) => ({ event, params }),
+);
+export const clearNotifications = createFsAction(CLEAR_NOTIFICATIONS);
-export const clearAuthenticationErrors = createAction(
+export const _authenticationRequest = createFsAction(AUTHENTICATION_REQUEST);
+export const _authenticationSuccess = createFsAction(AUTHENTICATION_SUCCESS);
+const _authenticationFailure = createFsAction(AUTHENTICATION_FAILURE);
+
+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 = createFsAction(REGISTRATION_ERRORS);
+const loginByEmailErrors = createFsAction(LOGIN_BY_EMAIL_ERRORS);
-const registrationErrors = createAction(REGISTRATION_ERRORS);
-const loginByEmailErrors = createAction(LOGIN_BY_EMAIL_ERRORS);
+export const setSpinnerDelayEnabled = createFsAction(SET_SPINNER_DELAY_ENABLED);
+export const setIncidentEnabled = createFsAction(SET_INCIDENT_ENABLED);
-export const setSpinnerDelayEnabled = createAction(SET_SPINNER_DELAY_ENABLED);
-export const setIncidentEnabled = createAction(SET_INCIDENT_ENABLED);
+export const startSound = createAction('START_SOUND');
+export const stopSound = createAction('STOP_SOUND');
function setBaseURL(baseURL) {
return (dispatch, getState) => {
diff --git a/src/redux/App/reducers.js b/src/redux/App/reducers.js
index 39a4ea6f1..d4044450a 100644
--- a/src/redux/App/reducers.js
+++ b/src/redux/App/reducers.js
@@ -2,11 +2,19 @@
/*
* App reducer, dealing with non-domain specific state
*/
+
import Config from 'react-native-config';
-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,
@@ -16,6 +24,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,
@@ -25,7 +34,6 @@ import {
LOGIN_BY_EMAIL_ERRORS,
LOGOUT_SUCCESS,
ONBOARDED,
- PUSH_NOTIFICATION,
REGISTER_PUSH_NOTIFICATION_TOKEN,
REGISTRATION_ERRORS,
RESET_MODAL,
@@ -50,6 +58,9 @@ import {
SET_USER,
} from './actions';
+import { EVENT as EVENT_ORDER } from '../../domain/Order';
+import { EVENT as EVENT_TASK_COLLECTION } from '../../domain/TaskCollection';
+
const initialState = {
customBuild: !!Config.DEFAULT_SERVER,
firstRun: true,
@@ -102,6 +113,37 @@ const initialState = {
isIncidentEnabled: false,
};
+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,11 +184,17 @@ export default (state = initialState, action = {}) => {
isCentrifugoConnected: false,
};
- case PUSH_NOTIFICATION:
- return {
- ...state,
- notifications: state.notifications.concat([action.payload]),
- };
+ 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 {
@@ -264,12 +312,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 {
diff --git a/src/redux/App/selectors.js b/src/redux/App/selectors.js
index 7f6e069c9..022d12f43 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;
@@ -102,9 +108,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 [];
@@ -148,3 +156,39 @@ export const selectIsSpinnerDelayEnabled = state =>
export const selectIsIncidentEnabled = state =>
state.app.isIncidentEnabled ?? false;
+
+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/Courier/taskActions.js b/src/redux/Courier/taskActions.js
index c12f9ae82..e6e45d1f2 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 { selectHttpClient } from '../App/selectors';
+import { selectCurrentRoute, selectHttpClient } from '../App/selectors';
/*
* Action Types
@@ -162,7 +162,20 @@ function showAlertAfterBulk(messages) {
* Thunk Creators
*/
-export function loadTasks(selectedDate, refresh = false) {
+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, cb) {
return function (dispatch, getState) {
const { httpClient } = getState().app;
@@ -192,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/Courier/taskEntityReducer.js b/src/redux/Courier/taskEntityReducer.js
index 7433cb59d..c6057401a 100644
--- a/src/redux/Courier/taskEntityReducer.js
+++ b/src/redux/Courier/taskEntityReducer.js
@@ -6,7 +6,7 @@ import {
BULK_ASSIGNMENT_TASKS_SUCCESS,
UNASSIGN_TASK_SUCCESS,
} from '../Dispatch/actions';
-import { MESSAGE } from '../middlewares/CentrifugoMiddleware';
+import { CENTRIFUGO_MESSAGE } from '../middlewares/CentrifugoMiddleware';
import {
ADD_PICTURE,
ADD_SIGNATURE,
@@ -239,7 +239,7 @@ export const tasksEntityReducer = (
}
return state;
- case MESSAGE:
+ case CENTRIFUGO_MESSAGE:
return processWsMsg(state, action);
case ADD_SIGNATURE:
diff --git a/src/redux/Courier/taskMiddlewares.js b/src/redux/Courier/taskMiddlewares.js
index 331a2b2ad..5ae1f2e87 100644
--- a/src/redux/Courier/taskMiddlewares.js
+++ b/src/redux/Courier/taskMiddlewares.js
@@ -1,9 +1,10 @@
import _ from 'lodash';
import { AppState } from 'react-native';
-import { LOGOUT_SUCCESS, pushNotification } from '../App/actions';
+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 }) => {
return next => action => {
@@ -54,8 +55,8 @@ export const ringOnTaskListUpdated = ({ getState, dispatch }) => {
if (addedTasks.length > 0 || removedTasks.length > 0) {
dispatch(
- pushNotification('tasks:changed', {
- date: state.date,
+ addNotification(EVENT_TASK_COLLECTION.CHANGED, {
+ date: date,
added: addedTasks,
removed: removedTasks,
}),
diff --git a/src/redux/Restaurant/__tests__/middlewares.test.js b/src/redux/Restaurant/__tests__/middlewares.test.js
index adc4902dd..e6d7df7b1 100644
--- a/src/redux/Restaurant/__tests__/middlewares.test.js
+++ b/src/redux/Restaurant/__tests__/middlewares.test.js
@@ -4,10 +4,10 @@ import AppUser from '../../../AppUser';
import appReducer from '../../App/reducers';
import { message as wsMessage } from '../../middlewares/CentrifugoMiddleware/actions';
import { loadOrderSuccess, loadOrdersSuccess } from '../actions';
-import { ringOnNewOrderCreated } from '../middlewares';
+import { notifyOnNewOrderCreated } from '../middlewares';
import restaurantReducer from '../reducers';
-describe('ringOnNewOrderCreated', () => {
+describe('notifyOnNewOrderCreated', () => {
beforeEach(() => {
jest.mock('react-native/Libraries/AppState/AppState', () => ({
currentState: 'active',
@@ -32,7 +32,7 @@ describe('ringOnNewOrderCreated', () => {
const store = createStore(
reducer,
preloadedState,
- applyMiddleware(ringOnNewOrderCreated),
+ applyMiddleware(notifyOnNewOrderCreated),
);
store.dispatch(
@@ -76,7 +76,7 @@ describe('ringOnNewOrderCreated', () => {
const store = createStore(
reducer,
preloadedState,
- applyMiddleware(ringOnNewOrderCreated),
+ applyMiddleware(notifyOnNewOrderCreated),
);
store.dispatch(loadOrderSuccess({ '@id': '/api/orders/1', state: 'new' }));
@@ -125,7 +125,7 @@ describe('ringOnNewOrderCreated', () => {
const store = createStore(
reducer,
preloadedState,
- applyMiddleware(thunk, ringOnNewOrderCreated),
+ applyMiddleware(thunk, notifyOnNewOrderCreated),
);
store.dispatch(
@@ -172,7 +172,7 @@ describe('ringOnNewOrderCreated', () => {
const store = createStore(
reducer,
preloadedState,
- applyMiddleware(ringOnNewOrderCreated),
+ 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 85467b347..25c9b7d03 100644
--- a/src/redux/Restaurant/actions.js
+++ b/src/redux/Restaurant/actions.js
@@ -2,14 +2,14 @@ import { CommonActions } from '@react-navigation/native';
import { Buffer } from 'buffer';
import _ from 'lodash';
import BleManager from 'react-native-ble-manager';
-import { createAction } from 'redux-actions';
+import { createAction, createAsyncThunk } from '@reduxjs/toolkit';
+import { createAction as createFsAction } from 'redux-actions';
import DropdownHolder from '../../DropdownHolder';
import NavigationHolder from '../../NavigationHolder';
import * as SunmiPrinterLibrary from '@mitsuharu/react-native-sunmi-printer-library';
import { encodeForPrinter } from '../../utils/order';
-import { pushNotification } from '../App/actions';
import i18n from '../../i18n';
@@ -19,6 +19,9 @@ import {
LOAD_MY_RESTAURANTS_SUCCESS,
} from '../App/actions';
+import { selectHttpClient } from '../App/selectors';
+import { selectOrderById } from './selectors';
+
/*
* Action Types
*/
@@ -114,86 +117,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,
@@ -202,39 +207,47 @@ 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');
+
+export const setPrintNumberOfCopies = createAction(
+ 'SET_PRINT_NUMBER_OF_COPIES',
+);
+
/*
* Thunk Creators
*/
@@ -388,23 +401,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();
@@ -432,6 +428,28 @@ export function acceptOrder(order, cb) {
};
}
+export const startPreparing = createAsyncThunk(
+ 'order/startPreparing',
+ async (order, thunkAPI) => {
+ const { getState } = thunkAPI;
+
+ const httpClient = selectHttpClient(getState());
+
+ return await httpClient.put(order['@id'] + '/start_preparing');
+ },
+);
+
+export const finishPreparing = createAsyncThunk(
+ 'order/finishPreparing',
+ async (order, thunkAPI) => {
+ const { getState } = thunkAPI;
+
+ const httpClient = selectHttpClient(getState());
+
+ return await httpClient.put(order['@id'] + '/finish_preparing');
+ },
+);
+
export function refuseOrder(order, reason, cb) {
return function (dispatch, getState) {
const { app } = getState();
@@ -637,8 +655,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 {
@@ -647,13 +680,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;
}
@@ -669,6 +705,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'),
@@ -742,14 +779,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'),
@@ -760,7 +799,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));
@@ -786,12 +825,12 @@ 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))
+ .catch(e => console.warn('disconnectPrinter', e))
.finally(() => {
dispatch(printerDisconnected());
diff --git a/src/redux/Restaurant/middlewares.js b/src/redux/Restaurant/middlewares.js
index cdc0c06af..a1cc42749 100644
--- a/src/redux/Restaurant/middlewares.js
+++ b/src/redux/Restaurant/middlewares.js
@@ -1,11 +1,12 @@
import _ from 'lodash';
import { AppState } from 'react-native';
-import { pushNotification } from '../App/actions';
+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 ringOnNewOrderCreated = ({ getState, dispatch }) => {
+export const notifyOnNewOrderCreated = ({ getState, dispatch }) => {
return next => action => {
if (AppState.currentState !== 'active') {
return next(action);
@@ -40,8 +41,8 @@ export const ringOnNewOrderCreated = ({ getState, dispatch }) => {
(a, b) => a['@id'] + ':' + a.state === b['@id'] + ':' + b.state,
);
orders.forEach(o => {
- if (o.state === 'new') {
- dispatch(pushNotification('order:created', { order: o }));
+ if (o.state === STATE.NEW) {
+ dispatch(addNotification(EVENT_ORDER.CREATED, { order: o }));
}
});
}
diff --git a/src/redux/Restaurant/reducers.js b/src/redux/Restaurant/reducers.js
index ebef9db0b..056909344 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,
@@ -58,6 +61,12 @@ import {
SET_NEXT_PRODUCTS_PAGE,
SUNMI_PRINTER_DETECTED,
UPDATE_LOOPEAT_FORMATS_SUCCESS,
+ finishPreparing,
+ printFulfilled,
+ printPending,
+ printRejected,
+ setPrintNumberOfCopies,
+ startPreparing,
} from './actions';
import {
@@ -66,10 +75,9 @@ import {
LOAD_MY_RESTAURANTS_SUCCESS,
} from '../App/actions';
-import { MESSAGE } from '../middlewares/CentrifugoMiddleware/actions';
+import { CENTRIFUGO_MESSAGE } from '../middlewares/CentrifugoMiddleware/actions';
-import _ from 'lodash';
-import moment from 'moment';
+import { EVENT as EVENT_ORDER, STATE } from '../../domain/Order';
const initialState = {
fetchError: null, // Error object describing the error
@@ -85,11 +93,30 @@ const initialState = {
menus: [],
bluetoothEnabled: false,
isScanningBluetooth: false,
+ /**
+ * Peripheral (react-native-ble-manager)
+ */
printer: null,
productOptions: [],
isSunmiPrinter: false,
bluetoothStarted: false,
loopeatFormats: {},
+ /**
+ * {
+ * [orderId]: {
+ * copiesToPrint: number,
+ * failedAttempts: number,
+ * }
+ * }
+ */
+ ordersToPrint: {},
+ printingOrderId: null,
+ preferences: {
+ autoAcceptOrders: {
+ printNumberOfCopies: 1,
+ printMaxFailedAttempts: 3,
+ },
+ },
};
const spliceOrders = (state, payload) => {
@@ -170,6 +197,34 @@ const spliceProductOptions = (state, payload) => {
return state;
};
+function updateOrdersToPrint(state, orderId) {
+ if (state.restaurant.autoAcceptOrdersEnabled) {
+ if (state.ordersToPrint[orderId]) {
+ return state;
+ }
+
+ const numberOfCopies =
+ state.preferences.autoAcceptOrders.printNumberOfCopies;
+
+ if (numberOfCopies === 0) {
+ return state;
+ }
+
+ return {
+ ...state,
+ ordersToPrint: {
+ ...state.ordersToPrint,
+ [orderId]: {
+ copiesToPrint: numberOfCopies,
+ failedAttempts: 0,
+ },
+ },
+ };
+ } else {
+ return state;
+ }
+}
+
export default (state = initialState, action = {}) => {
let newState;
@@ -182,6 +237,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:
@@ -201,6 +258,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:
@@ -278,6 +337,8 @@ export default (state = initialState, action = {}) => {
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),
@@ -346,7 +407,7 @@ export default (state = initialState, action = {}) => {
restaurant: action.payload,
};
- case DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS:
+ case DELETE_OPENING_HOURS_SPECIFICATION_SUCCESS: {
const { specialOpeningHoursSpecification } = state;
return {
@@ -362,6 +423,7 @@ export default (state = initialState, action = {}) => {
),
},
};
+ }
case CHANGE_STATUS_SUCCESS:
return {
@@ -463,35 +525,30 @@ export default (state = initialState, action = {}) => {
isSunmiPrinter: true,
};
- case MESSAGE:
+ case CENTRIFUGO_MESSAGE:
if (action.payload.name && action.payload.data) {
const { name, data } = action.payload;
switch (name) {
- case 'order:created':
- case 'order:accepted':
+ case EVENT_ORDER.CREATED:
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',
- };
- }
-
return {
...state,
- orders: addOrReplace(state, newOrder),
+ 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;
}
@@ -499,6 +556,83 @@ export default (state = initialState, action = {}) => {
return state;
+ case printPending.type:
+ return {
+ ...state,
+ printingOrderId: action.payload['@id'],
+ };
+
+ 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;
+ }
+
+ return {
+ ...state,
+ printingOrderId: null,
+ ordersToPrint: {
+ ...state.ordersToPrint,
+ [orderId]: {
+ ...printTask,
+ failedAttempts: printTask.failedAttempts + 1,
+ },
+ },
+ };
+ }
+
+ 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 e84e99797..00205ea3f 100644
--- a/src/redux/Restaurant/selectors.js
+++ b/src/redux/Restaurant/selectors.js
@@ -1,14 +1,18 @@
-import _, { find } from 'lodash';
-import moment from 'moment';
import { createSelector } from 'reselect';
+import { find } from 'lodash';
+import moment from 'moment';
+import _ from 'lodash';
import { matchesDate } from '../../utils/order';
+import { EVENT, STATE } from '../../domain/Order';
+
+export const selectRestaurant = state => state.restaurant.restaurant;
+export const selectDate = state => state.restaurant.date;
-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;
@@ -27,7 +31,7 @@ export const selectSpecialOpeningHoursSpecificationForToday = createSelector(
);
export const selectSpecialOpeningHoursSpecification = createSelector(
- state => state.restaurant.restaurant,
+ selectRestaurant,
restaurant => {
if (restaurant) {
return restaurant.specialOpeningHoursSpecification;
@@ -37,44 +41,105 @@ export const selectSpecialOpeningHoursSpecification = createSelector(
},
);
+export const selectAutoAcceptOrdersEnabled = createSelector(
+ selectRestaurant,
+ 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,
+ selectDate,
_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)],
),
);
+function isOrderPicked(order) {
+ return order.events.findIndex(ev => ev.type === EVENT.PICKED) !== -1;
+}
+
export const selectAcceptedOrders = createSelector(
- _selectDate,
+ selectDate,
+ _selectOrders,
+ (date, orders) =>
+ _.sortBy(
+ _.filter(
+ orders,
+ o =>
+ matchesDate(o, date) &&
+ o.state === STATE.ACCEPTED &&
+ !isOrderPicked(o),
+ ),
+ [o => moment.parseZone(o.pickupExpectedAt)],
+ ),
+);
+
+export const selectStartedOrders = createSelector(
+ selectDate,
+ _selectOrders,
+ (date, orders) =>
+ _.sortBy(
+ _.filter(
+ orders,
+ o =>
+ matchesDate(o, date) &&
+ o.state === STATE.STARTED &&
+ !isOrderPicked(o),
+ ),
+ [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 =>
+ matchesDate(o, date) && o.state === STATE.READY && !isOrderPicked(o),
),
[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 =>
+ matchesDate(o, date) &&
+ (o.state === STATE.ACCEPTED ||
+ o.state === STATE.STARTED ||
+ o.state === STATE.READY) &&
+ isOrderPicked(o),
),
[o => moment.parseZone(o.pickupExpectedAt)],
),
);
export const selectCancelledOrders = createSelector(
- _selectDate,
+ selectDate,
_selectOrders,
(date, orders) =>
_.sortBy(
@@ -82,15 +147,100 @@ 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)],
),
);
export const selectFulfilledOrders = createSelector(
- _selectDate,
+ selectDate,
_selectOrders,
(date, orders) =>
- _.filter(orders, o => matchesDate(o, date) && o.state === 'fulfilled'),
+ _.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,
+);
+
+const selectOrdersToPrint = state => state.restaurant.ordersToPrint;
+
+export const selectAutoAcceptOrdersPrintNumberOfCopies = state =>
+ state.restaurant.preferences.autoAcceptOrders.printNumberOfCopies;
+
+const selectAutoAcceptOrdersPrintMaxFailedAttempts = state =>
+ state.restaurant.preferences.autoAcceptOrders.printMaxFailedAttempts;
+
+export const selectOrderIdsToPrint = createSelector(
+ selectOrdersToPrint,
+ selectAutoAcceptOrdersPrintMaxFailedAttempts,
+ (ordersToPrint, printMaxFailedAttempts) => {
+ const orderIdsToPrint = [];
+
+ Object.keys(ordersToPrint).forEach(orderId => {
+ const printTask = ordersToPrint[orderId];
+ if (printTask.failedAttempts <= printMaxFailedAttempts) {
+ orderIdsToPrint.push(orderId);
+ }
+ });
+
+ return orderIdsToPrint;
+ },
+);
+
+export const selectOrderIdsFailedToPrint = createSelector(
+ selectOrdersToPrint,
+ selectAutoAcceptOrdersPrintMaxFailedAttempts,
+ (ordersToPrint, printMaxFailedAttempts) => {
+ const orderIdsFailedToPrint = [];
+
+ Object.keys(ordersToPrint).forEach(orderId => {
+ const printTask = ordersToPrint[orderId];
+ if (printTask.failedAttempts > printMaxFailedAttempts) {
+ orderIdsFailedToPrint.push(orderId);
+ }
+ });
+
+ return orderIdsFailedToPrint;
+ },
+);
+
+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;
+ }
+ },
+);
+
+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)
+ );
+ },
);
diff --git a/src/redux/middlewares/CentrifugoMiddleware/actions.js b/src/redux/middlewares/CentrifugoMiddleware/actions.js
index ae760151d..eb2f55259 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) {
return function (dispatch, getState) {
diff --git a/src/redux/middlewares/CentrifugoMiddleware/index.js b/src/redux/middlewares/CentrifugoMiddleware/index.js
index e0b6f3e6d..f02cd3a23 100644
--- a/src/redux/middlewares/CentrifugoMiddleware/index.js
+++ b/src/redux/middlewares/CentrifugoMiddleware/index.js
@@ -2,27 +2,40 @@ import Centrifuge from 'centrifuge';
import parseUrl from 'url-parse';
import {
+ CENTRIFUGO_MESSAGE,
CONNECT,
CONNECTED,
DISCONNECTED,
- MESSAGE,
connected,
disconnected,
message,
} from './actions';
import {
+ selectBaseURL,
selectHttpClient,
selectHttpClientHasCredentials,
selectIsAuthenticated,
selectUser,
} 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 => {
- // 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);
@@ -38,14 +51,14 @@ export default ({ getState, dispatch }) => {
}
const httpClient = selectHttpClient(state);
- const baseURL = state.app.baseURL;
+ 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';
- const centrifuge = new Centrifuge(
+ centrifuge = new Centrifuge(
`${protocol}://${url.hostname}/centrifugo/connection/websocket`,
{
debug: __DEV__,
@@ -80,4 +93,4 @@ export default ({ getState, dispatch }) => {
};
};
-export { CONNECTED, DISCONNECTED, MESSAGE, connected, disconnected };
+export { CENTRIFUGO_MESSAGE, CONNECTED, DISCONNECTED, connected, disconnected };
diff --git a/src/redux/middlewares/PushNotificationMiddleware/index.js b/src/redux/middlewares/PushNotificationMiddleware/index.js
index d0847278d..db945bcc5 100644
--- a/src/redux/middlewares/PushNotificationMiddleware/index.js
+++ b/src/redux/middlewares/PushNotificationMiddleware/index.js
@@ -1,7 +1,10 @@
import { Platform } from 'react-native';
+import moment from 'moment';
import {
LOGOUT_REQUEST,
deletePushNotificationTokenSuccess,
+ foregroundPushNotification,
+ registerPushNotificationToken,
savePushNotificationTokenSuccess,
} from '../../App/actions';
import {
@@ -9,14 +12,119 @@ import {
selectHttpClientHasCredentials,
selectIsAuthenticated,
} from '../../App/selectors';
-
-let isFetching = false;
+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 { loadTasks, navigateAndLoadTasks } from '../../Courier/taskActions';
// As remote push notifications are configured very early,
// most of the time the user won't be authenticated
// (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: {
+ const dateStr = event.data.date;
+
+ dispatch(
+ loadTasks(moment(dateStr), true, () => {
+ dispatch(
+ foregroundPushNotification(event.name, {
+ date: dateStr,
+ }),
+ );
+ }),
+ );
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ };
+
+ PushNotification.configure({
+ onRegister: token => onRegister(token),
+ onNotification: message => onNotification(message),
+ onBackgroundMessage: message => onBackgroundMessage(message),
+ });
+
+ let isFetching = false;
+
return next => action => {
const result = next(action);
const state = getState();
@@ -60,7 +168,6 @@ export default ({ getState, dispatch }) => {
isFetching = false;
});
}
-
return result;
};
};
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/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/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 = {
diff --git a/src/redux/store.js b/src/redux/store.js
index 87f9db7dc..12bb7962b 100644
--- a/src/redux/store.js
+++ b/src/redux/store.js
@@ -1,23 +1,23 @@
import { applyMiddleware, createStore } from 'redux';
-import ReduxAsyncQueue from 'redux-async-queue';
-import { composeWithDevTools } from 'redux-devtools-extension';
-import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk';
+import ReduxAsyncQueue from 'redux-async-queue';
import { persistStore } from 'redux-persist';
import Config from 'react-native-config';
-import { filterExpiredCarts } from './Checkout/middlewares';
-import { ringOnTaskListUpdated } from './Courier/taskMiddlewares';
-import { ringOnNewOrderCreated } from './Restaurant/middlewares';
-import BluetoothMiddleware from './middlewares/BluetoothMiddleware';
-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 reducers from './reducers';
+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,
@@ -28,6 +28,7 @@ const middlewares = [
CentrifugoMiddleware,
SentryMiddleware,
filterExpiredCarts,
+ SoundMiddleware,
];
if (!Config.DEFAULT_SERVER) {
@@ -35,22 +36,15 @@ if (!Config.DEFAULT_SERVER) {
...[
GeolocationMiddleware,
BluetoothMiddleware,
- ringOnNewOrderCreated,
+ notifyOnNewOrderCreated,
ringOnTaskListUpdated,
],
);
}
-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 12a9ade20..dcb9e53cd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13379,6 +13379,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"