├── .editorconfig
├── README.md
├── driver-app
├── .env
├── App.js
├── Root.js
├── app.json
├── index.js
├── package.json
└── src
│ ├── helpers
│ └── location.js
│ └── screens
│ ├── ContactCustomer.js
│ └── OrderMap.js
├── ordering-app
├── .env
├── App.js
├── GlobalContext.js
├── Root.js
├── app.json
├── index.js
├── package.json
└── src
│ ├── components
│ ├── ListCard.js
│ ├── NavHeaderRight.js
│ └── PageCard.js
│ ├── helpers
│ ├── getSubTotal.js
│ └── location.js
│ └── screens
│ ├── ContactDriver.js
│ ├── FoodDetails.js
│ ├── FoodList.js
│ ├── OrderSummary.js
│ └── TrackOrder.js
└── server
├── .env
├── data
└── foods.js
├── images
├── fried-mee-sua.jpg
├── gyoza.jpg
├── honey-garlic-chicken.jpg
├── pork-with-veggies.jpg
├── red-bbq-pork-noodles.jpg
├── rice-with-roasted-pork.jpg
├── salmon-sashimi.jpg
├── sesame-chicken-noodle.jpg
├── spicy-teriyaki.jpg
├── tori-karaage.jpg
├── vietnamese-pho.jpg
└── white-bee-hoon.jpg
├── index.js
└── package.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = false
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | quote_type = double
9 |
10 | [*.js]
11 | indent_style = space
12 | indent_size = 2
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-Native-Food-Delivery
2 | A food delivery app built with React Native, Pusher Channels, Chatkit, and Beams.
3 |
4 | You can read the tutorial here:
5 |
6 | - [Create a food ordering app in React Native - Part 1: Making an order](http://pusher.com/tutorials/food-ordering-app-react-native-part-1)
7 | - [Create a food ordering app in React Native - Part 2: Adding the driver app and chat functionality](http://pusher.com/tutorials/food-ordering-app-react-native-part-2)
8 | - [Create a food ordering app in React Native - Part 3: Adding push notifications](http://pusher.com/tutorials/food-ordering-app-react-native-part-3)
9 |
10 | It includes the following features:
11 |
12 | - Food ordering
13 | - Real-time location tracking
14 | - One on one chat
15 | - Push notifications
16 |
17 | Each branch contains the code on each part of the tutorial:
18 |
19 | - `starter` - contains the starting point of the delivery app.
20 | - `food-ordering` - the final output for part 1 of the series. This contains the initial code for the food ordering app.
21 | - `driver-app` - the final output of part 2 of the series. This contains the code for the driver app as well as the code for implementing Chat.
22 | - `push-notifications` - the final output of part 3 of the series. This contains the code for implementing push notifications.
23 | - `master` - contains the most updated code.
24 |
25 | ## Prerequisites
26 |
27 | - React Native development environment
28 | - [Node.js](https://nodejs.org/en/)
29 | - [Yarn](https://yarnpkg.com/en/)
30 | - [Google Cloud Console account](https://console.cloud.google.com/) - enable [Google Maps](https://developers.google.com/maps/gmp-get-started).
31 | - [Channels app instance](https://pusher.com/channels)
32 | - [Chatkit app instance](https://pusher.com/chatkit)
33 | - [Firebase app instance](https://console.firebase.google.com/) - one for each app (food delivery and driver app).
34 | - [Beams app instance](https://pusher.com/beams) - one for each app.
35 | - [ngrok account](https://ngrok.com/)
36 |
37 | ## Getting started
38 |
39 | 1. Clone the repo:
40 |
41 | ```
42 | git clone https://github.com/anchetaWern/React-Native-Food-Delivery.git
43 | ```
44 |
45 | 2. Create a new React Native app for each project
46 |
47 | ```
48 | react-native init RNFoodDelivery
49 | react-native init RNFoodDeliveryDriver
50 | ```
51 |
52 | 3. Copy the contents of the `ordering-app` folder in the repo to the `RNFoodDelivery` project. Do the same for the `driver-app` folder and copy its contents over to the `RNFoodDeliveryDriver` project. Then move the `server` folder to your working directory.
53 |
54 | 4. Install the dependencies for both projects as well as the server:
55 |
56 | ```
57 | cd RNFoodDelivery
58 | yarn install
59 | ```
60 |
61 | ```
62 | cd RNFoodDeliveryDriver
63 | yarn install
64 | ```
65 |
66 | ```
67 | cd server
68 | yarn install
69 | ```
70 |
71 | 5. Update the `.env` file on both projects with your credentials. Do the same for the `server/.env` file.
72 |
73 | 6. Link the following packages manually:
74 |
75 | - react-native-permissions
76 | - react-native-config
77 | - react-native-google-places
78 |
79 | 7. Update the `android/app/src/main/AndroidManifest.xml` file with the required permissions and Google API key:
80 |
81 | ```
82 |
84 |
85 |
86 |
87 |
88 |
89 | ...
90 |
91 | ```
92 |
93 | ```
94 |
95 |
98 |
99 | ```
100 |
101 | 8. Run the server:
102 |
103 | ```
104 | cd server
105 | node index.js
106 | ```
107 |
108 | 9. Expose the server using ngrok
109 |
110 | ```
111 | ~/ngrok http 5000
112 | ```
113 |
114 | 10. Run the two apps. The first instance runs the bundler on a different port from the default one. Be sure to set the port (from the dev settings) then disconnect the device before running the second app:
115 |
116 | ```
117 | cd RNFoodDelivery
118 | react-native start --port=8080
119 | react-native run-android
120 | ```
121 |
122 | ```
123 | cd RNFoodDeliveryDriver
124 | react-native start
125 | react-native run-android
126 | ```
127 |
128 | ## Built with
129 |
130 | - [React Native](https://facebook.github.io/react-native/)
131 | - [Channels](https://pusher.com/channels)
132 | - [Chatkit](https://pusher.com/chatkit)
133 | - [Beams](https://pusher.com/beams)
134 | - [React Native Maps](https://github.com/react-native-community/react-native-maps)
135 |
136 | ## Donation
137 |
138 | If this project helped you reduce time to develop, please consider buying me a cup of coffee :)
139 |
140 |
141 |
--------------------------------------------------------------------------------
/driver-app/.env:
--------------------------------------------------------------------------------
1 | CHANNELS_APP_KEY="YOUR CHANNELS APP KEY"
2 | CHANNELS_APP_CLUSTER="YOUR CHANNELS APP CLUSTER"
3 |
4 | CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT INSTANCE LOCATOR ID"
5 | CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET KEY"
6 | CHATKIT_TOKEN_PROVIDER_ENDPOINT="YOUR CHATKIT TOKEN PROVIDER ENDPOINT"
7 |
8 | GOOGLE_API_KEY="YOUR GOOGLE API KEY"
9 |
10 | NGROK_HTTPS_URL="YOUR NGROK HTTPS URL"
11 |
12 | BEAMS_INSTANCE_ID="YOUR DRIVER APP BEAMS INSTANCE ID"
--------------------------------------------------------------------------------
/driver-app/App.js:
--------------------------------------------------------------------------------
1 | import React, {Fragment} from 'react';
2 | import {SafeAreaView, StatusBar, View, StyleSheet} from 'react-native';
3 |
4 | import Root from './Root';
5 |
6 | const App = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | //
18 |
19 | const styles = StyleSheet.create({
20 | container: {
21 | flex: 1,
22 | },
23 | });
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/driver-app/Root.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {YellowBox} from 'react-native';
3 |
4 | import {createAppContainer} from 'react-navigation';
5 | import {createStackNavigator} from 'react-navigation-stack';
6 |
7 | import OrderMap from './src/screens/OrderMap';
8 | import ContactCustomer from './src/screens/ContactCustomer';
9 |
10 | YellowBox.ignoreWarnings(['Setting a timer']);
11 |
12 | const RootStack = createStackNavigator(
13 | {
14 | OrderMap,
15 | ContactCustomer,
16 | },
17 | {
18 | initialRouteName: 'OrderMap',
19 | },
20 | );
21 |
22 | const AppContainer = createAppContainer(RootStack);
23 |
24 | class Router extends Component {
25 | render() {
26 | return ;
27 | }
28 | }
29 |
30 | export default Router;
31 |
--------------------------------------------------------------------------------
/driver-app/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RNFoodDeliveryDriver",
3 | "displayName": "RNFoodDeliveryDriver"
4 | }
--------------------------------------------------------------------------------
/driver-app/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import {AppRegistry} from 'react-native';
6 | import App from './App';
7 | import {name as appName} from './app.json';
8 |
9 | AppRegistry.registerComponent(appName, () => App);
10 |
--------------------------------------------------------------------------------
/driver-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RNFoodDeliveryDriver",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "start": "react-native start",
9 | "test": "jest",
10 | "lint": "eslint ."
11 | },
12 | "dependencies": {
13 | "@pusher/chatkit-client": "^1.13.1",
14 | "@react-native-community/netinfo": "^4.4.0",
15 | "axios": "^0.19.0",
16 | "pusher-js": "^5.0.2",
17 | "react": "16.9.0",
18 | "react-native": "0.61.2",
19 | "react-native-config": "^0.11.7",
20 | "react-native-geocoding": "^0.4.0",
21 | "react-native-geolocation-service": "^3.1.0",
22 | "react-native-gesture-handler": "^1.4.1",
23 | "react-native-gifted-chat": "^0.11.0",
24 | "react-native-maps": "0.25.0",
25 | "react-native-maps-directions": "^1.7.3",
26 | "react-native-modal": "^11.4.0",
27 | "react-native-permissions": "^2.0.2",
28 | "react-native-reanimated": "^1.3.0",
29 | "react-native-screens": "^1.0.0-alpha.23",
30 | "react-navigation": "^4.0.10",
31 | "react-navigation-stack": "^1.9.4",
32 | "react-native-pusher-push-notifications": "git+http://git@github.com/ZeptInc/react-native-pusher-push-notifications#v.2.4.0-zept-master"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "^7.6.3",
36 | "@babel/runtime": "^7.6.3",
37 | "@react-native-community/eslint-config": "^0.0.5",
38 | "babel-jest": "^24.9.0",
39 | "eslint": "^6.5.1",
40 | "jest": "^24.9.0",
41 | "metro-react-native-babel-preset": "^0.56.0",
42 | "react-test-renderer": "16.9.0"
43 | },
44 | "jest": {
45 | "preset": "react-native"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/driver-app/src/helpers/location.js:
--------------------------------------------------------------------------------
1 | export function regionFrom(lat, lon, accuracy) {
2 | const oneDegreeOfLongitudeInMeters = 111.32 * 1000;
3 | const circumference = (40075 / 360) * 1000;
4 |
5 | const latDelta = accuracy * (1 / (Math.cos(lat) * circumference));
6 | const lonDelta = accuracy / oneDegreeOfLongitudeInMeters;
7 |
8 | return {
9 | latitude: lat,
10 | longitude: lon,
11 | latitudeDelta: Math.max(0, latDelta),
12 | longitudeDelta: Math.max(0, lonDelta),
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/driver-app/src/screens/ContactCustomer.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {View, StyleSheet} from 'react-native';
3 |
4 | import {GiftedChat} from 'react-native-gifted-chat';
5 | import {ChatManager, TokenProvider} from '@pusher/chatkit-client';
6 |
7 | import Config from 'react-native-config';
8 |
9 | const CHATKIT_INSTANCE_LOCATOR_ID = Config.CHATKIT_INSTANCE_LOCATOR_ID;
10 | const CHATKIT_SECRET_KEY = Config.CHATKIT_SECRET_KEY;
11 | const CHATKIT_TOKEN_PROVIDER_ENDPOINT = Config.CHATKIT_TOKEN_PROVIDER_ENDPOINT;
12 |
13 | class ContactCustomer extends Component {
14 | static navigationOptions = ({navigation}) => {
15 | return {
16 | title: 'Contact Customer',
17 | };
18 | };
19 |
20 | state = {
21 | messages: [],
22 | };
23 |
24 | constructor(props) {
25 | super(props);
26 | this.user_id = this.props.navigation.getParam('user_id');
27 | this.room_id = this.props.navigation.getParam('room_id');
28 | }
29 |
30 | async componentDidMount() {
31 | try {
32 | const chatManager = new ChatManager({
33 | instanceLocator: CHATKIT_INSTANCE_LOCATOR_ID,
34 | userId: this.user_id,
35 | tokenProvider: new TokenProvider({
36 | url: CHATKIT_TOKEN_PROVIDER_ENDPOINT,
37 | }),
38 | });
39 |
40 | let currentUser = await chatManager.connect();
41 | this.currentUser = currentUser;
42 |
43 | await this.currentUser.subscribeToRoomMultipart({
44 | roomId: this.room_id,
45 | hooks: {
46 | onMessage: this._onMessage,
47 | },
48 | messageLimit: 30,
49 | });
50 | } catch (err) {
51 | console.log('chatkit error: ', err);
52 | }
53 | }
54 |
55 | _onMessage = data => {
56 | const {message} = this._getMessage(data);
57 |
58 | this.setState(previousState => ({
59 | messages: GiftedChat.append(previousState.messages, message),
60 | }));
61 | };
62 |
63 | _getMessage = ({id, sender, parts, createdAt}) => {
64 | const text = parts.find(part => part.partType === 'inline').payload.content;
65 |
66 | const msg_data = {
67 | _id: id,
68 | text: text,
69 | createdAt: new Date(createdAt),
70 | user: {
71 | _id: sender.id.toString(),
72 | name: sender.name,
73 | avatar: `https://na.ui-avatars.com/api/?name=${sender.name}`,
74 | },
75 | };
76 |
77 | return {
78 | message: msg_data,
79 | };
80 | };
81 |
82 | render() {
83 | const {messages} = this.state;
84 |
85 | return (
86 |
87 | this._onSend(messages)}
90 | showUserAvatar={true}
91 | user={{
92 | _id: this.user_id,
93 | }}
94 | />
95 |
96 | );
97 | }
98 |
99 | _onSend = async ([message]) => {
100 | try {
101 | await this.currentUser.sendSimpleMessage({
102 | roomId: this.room_id,
103 | text: message.text,
104 | });
105 | } catch (send_msg_err) {
106 | console.log('error sending message: ', send_msg_err);
107 | }
108 | };
109 | }
110 | //
111 |
112 | const styles = StyleSheet.create({
113 | wrapper: {
114 | flex: 1,
115 | },
116 | });
117 |
118 | export default ContactCustomer;
119 |
--------------------------------------------------------------------------------
/driver-app/src/screens/OrderMap.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | View,
4 | Text,
5 | Button,
6 | TouchableOpacity,
7 | Alert,
8 | StyleSheet,
9 | } from 'react-native';
10 |
11 | import MapView from 'react-native-maps';
12 | import Pusher from 'pusher-js/react-native';
13 |
14 | import {check, request, PERMISSIONS, RESULTS} from 'react-native-permissions';
15 |
16 | import Geolocation from 'react-native-geolocation-service';
17 | import Modal from 'react-native-modal';
18 | import Config from 'react-native-config';
19 |
20 | import MapViewDirections from 'react-native-maps-directions';
21 | import axios from 'axios';
22 | import RNPusherPushNotifications from 'react-native-pusher-push-notifications';
23 |
24 | import {regionFrom} from '../helpers/location';
25 |
26 | const CHANNELS_APP_KEY = Config.CHANNELS_APP_KEY;
27 | const CHANNELS_APP_CLUSTER = Config.CHANNELS_APP_CLUSTER;
28 | const BASE_URL = Config.NGROK_HTTPS_URL;
29 |
30 | const GOOGLE_API_KEY = Config.GOOGLE_API_KEY;
31 |
32 | RNPusherPushNotifications.setInstanceId(Config.BEAMS_INSTANCE_ID);
33 |
34 | const subscribeToRoom = room_id => {
35 | RNPusherPushNotifications.subscribe(
36 | room_id,
37 | (statusCode, response) => {
38 | console.error(statusCode, response);
39 | },
40 | () => {
41 | console.log('Success');
42 | },
43 | );
44 | };
45 |
46 | const triggerNotification = async (room_id, push_type, data) => {
47 | try {
48 | await axios.post(`${BASE_URL}/push/${room_id}`, {
49 | push_type,
50 | data,
51 | });
52 | } catch (err) {
53 | console.log('error triggering notification: ', err);
54 | }
55 | };
56 |
57 | class OrderMap extends Component {
58 | static navigationOptions = ({navigation}) => {
59 | const showHeaderButton = navigation.getParam('showHeaderButton');
60 | return {
61 | title: 'Order Map',
62 | headerRight: showHeaderButton ? (
63 |
64 |
69 |
70 | ) : null,
71 | };
72 | };
73 |
74 | state = {
75 | locationPermission: 'undetermined',
76 | isOrderDetailsModalVisible: false,
77 | customer: null, // customer info
78 | currentLocation: null, // driver's current location
79 | hasOrder: false, // whether the driver is currently handling an order or not
80 | restaurantAddress: '',
81 | customerAddress: '',
82 | };
83 |
84 | constructor(props) {
85 | super(props);
86 |
87 | this.user_id = 'johndoe';
88 | this.user_name = 'John Doe';
89 | this.user_type = 'driver';
90 |
91 | this.available_drivers_channel = null; // this is where customer will send a request to any available driver
92 |
93 | this.ride_channel = null; // the channel used for communicating the current location
94 | // for a specific order. Channel name is the customer's username
95 |
96 | this.pusher = null; // the pusher client
97 | }
98 |
99 | async componentDidMount() {
100 | this.props.navigation.setParams({
101 | headerButtonLabel: 'Picked Order',
102 | headerButtonAction: this._pickedOrder,
103 | });
104 |
105 | this.pusher = new Pusher(CHANNELS_APP_KEY, {
106 | authEndpoint: `${BASE_URL}/pusher/auth`,
107 | cluster: CHANNELS_APP_CLUSTER,
108 | encrypted: true,
109 | });
110 |
111 | this.available_drivers_channel = this.pusher.subscribe(
112 | 'private-available-drivers',
113 | ); // subscribe to "available-drivers" channel
114 |
115 | this.available_drivers_channel.bind('pusher:subscription_succeeded', () => {
116 | this.available_drivers_channel.bind(
117 | 'client-driver-request',
118 | order_data => {
119 | if (!this.state.hasOrder) {
120 | // if the driver has currently no order
121 | this.setState({
122 | isOrderDetailsModalVisible: true,
123 | customer: order_data.customer,
124 | restaurantLocation: {
125 | latitude: order_data.restaurant_location[0],
126 | longitude: order_data.restaurant_location[1],
127 | },
128 | customerLocation: order_data.customer_location,
129 |
130 | restaurantAddress: order_data.restaurant_address,
131 | customerAddress: order_data.customer_address,
132 | });
133 | }
134 | },
135 | );
136 | });
137 |
138 | let location_permission = await check(
139 | PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
140 | );
141 |
142 | if (location_permission === 'denied') {
143 | location_permission = await request(
144 | PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
145 | );
146 | }
147 |
148 | if (location_permission === 'granted') {
149 | Geolocation.getCurrentPosition(
150 | position => {
151 | const {latitude, longitude, accuracy} = position.coords;
152 | const initialRegion = regionFrom(latitude, longitude, accuracy);
153 |
154 | this.setState({
155 | initialRegion,
156 | });
157 | },
158 | error => {
159 | console.log(error.code, error.message);
160 | },
161 | {enableHighAccuracy: true, timeout: 15000, maximumAge: 10000},
162 | );
163 |
164 | this.watch_location_id = Geolocation.watchPosition(
165 | position => {
166 | this.setState({
167 | currentLocation: position.coords,
168 | });
169 |
170 | if (this.state.hasOrder) {
171 | this.ride_channel.trigger('client-driver-location', {
172 | latitude: position.coords.latitude,
173 | longitude: position.coords.longitude,
174 | accuracy: position.coords.accuracy,
175 | });
176 | }
177 | },
178 | error => {
179 | console.log(error.code, error.message);
180 | },
181 | {enableHighAccuracy: true},
182 | );
183 | }
184 |
185 | this.setState({
186 | locationPermission: location_permission,
187 | });
188 |
189 | RNPusherPushNotifications.on('notification', noty => {
190 | Alert.alert(noty.title, noty.body);
191 | });
192 |
193 | try {
194 | await axios.post(`${BASE_URL}/login`, {
195 | user_id: this.user_id,
196 | user_name: this.user_name,
197 | user_type: this.user_type,
198 | });
199 | } catch (err) {
200 | console.log('error creating user: ', err);
201 | }
202 | }
203 |
204 |
205 | render() {
206 | const {
207 | isOrderDetailsModalVisible,
208 | restaurantAddress,
209 | customerAddress,
210 |
211 | currentLocation, // driver's current location
212 | restaurantLocation,
213 | customerLocation,
214 | initialRegion,
215 | } = this.state;
216 |
217 | return (
218 |
219 |
220 | {currentLocation && (
221 |
228 | )}
229 |
230 | {currentLocation && restaurantLocation && (
231 |
238 | )}
239 |
240 | {restaurantLocation && customerLocation && (
241 |
248 | )}
249 |
250 | {restaurantLocation && (
251 |
259 | )}
260 |
261 | {customerLocation && (
262 |
270 | )}
271 |
272 |
273 |
274 |
279 |
280 |
281 |
282 | {restaurantAddress && (
283 |
284 |
285 | Close
286 |
287 |
288 |
289 | Pick up
290 |
291 | {restaurantAddress.replace(',', '\n')}
292 |
293 |
294 |
295 |
296 | Drop off
297 |
298 | {customerAddress.replace(',', '\n')}
299 |
300 |
301 |
302 |
303 |
308 |
309 |
310 |
311 |
316 |
317 |
318 |
319 | )}
320 |
321 |
322 | );
323 | }
324 | //
325 |
326 | _pickedOrder = async () => {
327 | this.props.navigation.setParams({
328 | headerButtonLabel: 'Delivered Order',
329 | headerButtonAction: this._deliveredOrder,
330 | });
331 |
332 | this.ride_channel.trigger('client-order-update', {
333 | step: 2,
334 | });
335 |
336 | try {
337 | await axios.post(`${BASE_URL}/room`, {
338 | room_id: this.room_id,
339 | room_name: this.room_name,
340 | user_id: this.user_id,
341 | });
342 | } catch (room_err) {
343 | console.log('room error: ', room_err);
344 | }
345 |
346 | await triggerNotification(
347 | this.room_id,
348 | 'driver_picked_order',
349 | this.username,
350 | );
351 | };
352 |
353 | _deliveredOrder = async () => {
354 | this.ride_channel.unbind('client-driver-response');
355 | this.pusher.unsubscribe('private-ride-' + this.state.customer.username);
356 |
357 | this.setState({
358 | hasOrder: false,
359 |
360 | customer: null,
361 | restaurantLocation: null,
362 | customerLocation: null,
363 |
364 | restaurantAddress: null,
365 | customerAddress: null,
366 | });
367 |
368 | this.ride_channel.trigger('client-order-update', {
369 | step: 3,
370 | });
371 |
372 | await triggerNotification(
373 | this.room_id,
374 | 'driver_delivered_order',
375 | this.user_name,
376 | );
377 | };
378 |
379 | _contactCustomer = () => {
380 | this.props.navigation.navigate('ContactCustomer', {
381 | user_id: this.user_id,
382 | room_id: this.room_id,
383 | });
384 | };
385 |
386 | _acceptOrder = () => {
387 | const {customer, currentLocation} = this.state;
388 |
389 | this.setState({
390 | isOrderDetailsModalVisible: false,
391 | });
392 |
393 | this.ride_channel = this.pusher.subscribe(
394 | 'private-ride-' + customer.username,
395 | );
396 |
397 | this.ride_channel.bind('pusher:subscription_succeeded', () => {
398 | // send a handshake event to the customer
399 | this.ride_channel.trigger('client-driver-response', {
400 | response: 'yes', // yes, I'm available
401 | });
402 |
403 | // listen for the acknowledgement from the customer
404 | this.ride_channel.bind(
405 | 'client-driver-response',
406 | async customer_response => {
407 | if (customer_response.response == 'yes') {
408 | this.setState({
409 | hasOrder: true,
410 | });
411 |
412 | this.props.navigation.setParams({
413 | showHeaderButton: true,
414 | });
415 |
416 | const {room_id, room_name} = customer_response;
417 |
418 | this.room_id = room_id; // chat room ID
419 | this.room_name = room_name;
420 |
421 | subscribeToRoom(room_id);
422 |
423 | await triggerNotification(
424 | room_id,
425 | 'driver_accepted_order',
426 | this.username,
427 | );
428 |
429 | this.ride_channel.trigger('client-found-driver', {
430 | driver: {
431 | name: this.user_name,
432 | },
433 | location: {
434 | latitude: currentLocation.latitude,
435 | longitude: currentLocation.longitude,
436 | accuracy: currentLocation.accuracy,
437 | },
438 | });
439 |
440 | setTimeout(() => {
441 | this.ride_channel.trigger('client-order-update', {
442 | step: 1,
443 | });
444 | }, 2000);
445 | } else {
446 | Alert.alert(
447 | 'Order no longer available',
448 | 'Someone else already took the order. Or the customer cancelled.',
449 | [
450 | {
451 | text: 'Ok',
452 | },
453 | ],
454 | {cancelable: false},
455 | );
456 | }
457 | },
458 | );
459 | });
460 | };
461 |
462 | _declineOrder = () => {
463 | // homework: add code for informing the customer that the driver declined
464 | };
465 |
466 | _hideOrderDetailsModal = () => {
467 | this.setState({
468 | isOrderDetailsModalVisible: false,
469 | });
470 | // homework: add code for informing the customer that the driver declined
471 | };
472 |
473 | componentWillUnmount() {
474 | Geolocation.clearWatch(this.watch_location_id);
475 | }
476 | }
477 | //
478 |
479 | const styles = StyleSheet.create({
480 | navHeaderRight: {
481 | marginRight: 10,
482 | },
483 | wrapper: {
484 | flex: 1,
485 | },
486 | map: {
487 | ...StyleSheet.absoluteFillObject,
488 | },
489 | floatingButtonContainer: {
490 | position: 'absolute',
491 | bottom: '2%',
492 | left: '2%',
493 | alignSelf: 'flex-end',
494 | },
495 | modal: {
496 | flex: 1,
497 | backgroundColor: '#FFF',
498 | padding: 20,
499 | },
500 | close: {
501 | alignSelf: 'flex-end',
502 | marginBottom: 10,
503 | color: '#0366d6',
504 | },
505 | modalBody: {
506 | marginTop: 20,
507 | },
508 | addressContainer: {
509 | marginBottom: 20,
510 | },
511 | labelText: {
512 | fontSize: 18,
513 | fontWeight: 'bold',
514 | },
515 | valueText: {
516 | fontSize: 16,
517 | color: '#333',
518 | },
519 | buttonContainer: {
520 | marginBottom: 10,
521 | },
522 | });
523 |
524 | export default OrderMap;
--------------------------------------------------------------------------------
/ordering-app/.env:
--------------------------------------------------------------------------------
1 | CHANNELS_APP_KEY="YOUR CHANNELS APP KEY"
2 | CHANNELS_APP_CLUSTER="YOUR CHANNELS APP CLUSTER"
3 | GOOGLE_API_KEY="YOUR GOOGLE API KEY"
4 |
5 | NGROK_HTTPS_URL="YOUR NGROK HTTPS URL"
6 |
7 | CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT INSTANCE LOCATOR ID"
8 | CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET KEY"
9 | CHATKIT_TOKEN_PROVIDER_ENDPOINT="YOUR CHATKIT TOKEN PROVIDER ENDPOINT"
10 |
11 | BEAMS_INSTANCE_ID="YOUR ORDERING APP BEAMS INSTANCE ID"
--------------------------------------------------------------------------------
/ordering-app/App.js:
--------------------------------------------------------------------------------
1 | import React, {Fragment} from 'react';
2 | import {SafeAreaView, StatusBar, View, StyleSheet} from 'react-native';
3 |
4 | import Root from './Root';
5 |
6 | const App = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | //
18 |
19 | const styles = StyleSheet.create({
20 | container: {
21 | flex: 1,
22 | },
23 | });
24 |
25 | export default App;
26 |
--------------------------------------------------------------------------------
/ordering-app/GlobalContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const AppContext = React.createContext({});
3 |
4 | export class AppContextProvider extends React.Component {
5 | state = {
6 | cart_items: [],
7 | user_id: 'wernancheta',
8 | user_name: 'Wern Ancheta',
9 | user_type: 'customer',
10 | room_id: '',
11 | room_name: '',
12 | };
13 |
14 | constructor(props) {
15 | super(props);
16 | }
17 |
18 | setRoom = (id, name) => {
19 | this.setState({
20 | room_id: id,
21 | room_name: name,
22 | });
23 | };
24 |
25 | addToCart = (item, qty) => {
26 | let found = this.state.cart_items.filter(el => el.id === item.id);
27 | if (found.length == 0) {
28 | this.setState(prevState => {
29 | return {cart_items: prevState.cart_items.concat({...item, qty})};
30 | });
31 | } else {
32 | this.setState(prevState => {
33 | const other_items = prevState.cart_items.filter(
34 | el => el.id !== item.id,
35 | );
36 | return {
37 | cart_items: [...other_items, {...found[0], qty: found[0].qty + qty}],
38 | };
39 | });
40 | }
41 | };
42 |
43 | render() {
44 | return (
45 |
51 | {this.props.children}
52 |
53 | );
54 | }
55 | }
56 | //
57 |
58 | export const withAppContextProvider = ChildComponent => props => (
59 |
60 |
61 |
62 | );
--------------------------------------------------------------------------------
/ordering-app/Root.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {YellowBox} from 'react-native';
3 |
4 | import {createAppContainer} from 'react-navigation';
5 | import {createStackNavigator} from 'react-navigation-stack';
6 |
7 | import FoodList from './src/screens/FoodList';
8 | import FoodDetails from './src/screens/FoodDetails';
9 | import OrderSummary from './src/screens/OrderSummary';
10 | import TrackOrder from './src/screens/TrackOrder';
11 | import ContactDriver from './src/screens/ContactDriver';
12 |
13 | YellowBox.ignoreWarnings(['Setting a timer']);
14 |
15 | const RootStack = createStackNavigator(
16 | {
17 | FoodList,
18 | FoodDetails,
19 | OrderSummary,
20 | TrackOrder,
21 | ContactDriver,
22 | },
23 | {
24 | initialRouteName: 'FoodList',
25 | },
26 | );
27 |
28 | const AppContainer = createAppContainer(RootStack);
29 |
30 | class Router extends Component {
31 | render() {
32 | return ;
33 | }
34 | }
35 |
36 | export default Router;
37 |
--------------------------------------------------------------------------------
/ordering-app/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RNFoodDelivery",
3 | "displayName": "RNFoodDelivery"
4 | }
--------------------------------------------------------------------------------
/ordering-app/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import {AppRegistry} from 'react-native';
6 | import App from './App';
7 | import {name as appName} from './app.json';
8 | import {withAppContextProvider} from './GlobalContext';
9 |
10 | AppRegistry.registerComponent(appName, () => withAppContextProvider(App));
11 |
--------------------------------------------------------------------------------
/ordering-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "RNFoodDelivery",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "ios": "react-native run-ios",
8 | "start": "react-native start",
9 | "test": "jest",
10 | "lint": "eslint ."
11 | },
12 | "dependencies": {
13 | "@pusher/chatkit-client": "^1.13.1",
14 | "@react-native-community/netinfo": "^4.4.0",
15 | "axios": "^0.19.0",
16 | "pusher-js": "^5.0.2",
17 | "react": "16.9.0",
18 | "react-native": "0.61.1",
19 | "react-native-config": "^0.11.7",
20 | "react-native-geocoding": "^0.4.0",
21 | "react-native-geolocation-service": "^3.1.0",
22 | "react-native-gesture-handler": "^1.4.1",
23 | "react-native-gifted-chat": "^0.11.0",
24 | "react-native-google-places": "^3.1.2",
25 | "react-native-maps": "0.25.0",
26 | "react-native-maps-directions": "^1.7.3",
27 | "react-native-permissions": "^2.0.2",
28 | "react-native-pusher-push-notifications": "git+http://git@github.com/ZeptInc/react-native-pusher-push-notifications#v.2.4.0-zept-master",
29 | "react-native-reanimated": "^1.3.0",
30 | "react-native-screens": "^1.0.0-alpha.23",
31 | "react-native-simple-stepper": "^3.0.0",
32 | "react-native-vector-icons": "^6.6.0",
33 | "react-navigation": "^4.0.10",
34 | "react-navigation-stack": "^1.9.1",
35 | "string-random": "^0.1.3"
36 | },
37 | "devDependencies": {
38 | "@babel/core": "7.6.2",
39 | "@babel/runtime": "7.6.2",
40 | "@react-native-community/eslint-config": "0.0.3",
41 | "babel-jest": "24.9.0",
42 | "eslint": "6.4.0",
43 | "jest": "24.9.0",
44 | "metro-react-native-babel-preset": "0.51.1",
45 | "react-test-renderer": "16.9.0"
46 | },
47 | "jest": {
48 | "preset": "react-native"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/ordering-app/src/components/ListCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {TouchableOpacity, View, Image, Text, StyleSheet} from 'react-native';
3 |
4 | const ListCard = ({item, viewItem}) => {
5 | return (
6 | {
8 | viewItem(item);
9 | }}>
10 |
11 |
12 |
16 |
17 |
18 | {item.name}
19 | ${item.price}
20 |
21 |
22 |
23 | );
24 | //
25 | };
26 |
27 | const styles = StyleSheet.create({
28 | wrapper: {
29 | flexDirection: 'row',
30 | marginBottom: 15,
31 | },
32 | imageWrapper: {
33 | marginRight: 10,
34 | },
35 | image: {
36 | width: 100,
37 | height: 100,
38 | },
39 | title: {
40 | fontSize: 18,
41 | fontWeight: 'bold',
42 | },
43 | subtitle: {
44 | fontSize: 16,
45 | color: '#303540',
46 | },
47 | });
48 |
49 | export default ListCard;
50 |
--------------------------------------------------------------------------------
/ordering-app/src/components/NavHeaderRight.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, Button, StyleSheet} from 'react-native';
3 | import {withNavigation} from 'react-navigation';
4 |
5 | const NavHeaderRight = ({navigation, toScreen, buttonText}) => {
6 | return (
7 |
8 |
14 | );
15 | };
16 |
17 | const styles = StyleSheet.create({
18 | headerButtonContainer: {
19 | marginRight: 10,
20 | },
21 | });
22 |
23 | export default withNavigation(NavHeaderRight);
24 |
--------------------------------------------------------------------------------
/ordering-app/src/components/PageCard.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {View, Text, Image, Button, Dimensions, StyleSheet} from 'react-native';
3 |
4 | import Config from 'react-native-config';
5 |
6 | import {SimpleStepper} from 'react-native-simple-stepper';
7 | const screenWidth = Dimensions.get('window').width;
8 |
9 | const BASE_URL = Config.NGROK_HTTPS_URL;
10 |
11 | const PageCard = ({item, qty, qtyChanged, addToCart}) => {
12 | const {id, image, price} = item;
13 | return (
14 |
15 |
19 |
20 |
21 | {item.name}
22 |
23 |
24 |
25 | by {item.restaurant.name}
26 |
27 |
28 |
29 | ${price}
30 |
31 |
32 |
33 | How many?
34 |
35 |
36 |
37 | qtyChanged(value)}
39 | initialValue={1}
40 | minimumValue={1}
41 | maximumValue={10}
42 | showText={true}
43 | containerStyle={styles.stepperContainer}
44 | incrementImageStyle={styles.stepperButton}
45 | decrementImageStyle={styles.stepperButton}
46 | textStyle={styles.stepperText}
47 | />
48 |
49 |
50 |
51 |
59 |
60 | );
61 | };
62 |
63 | const styles = StyleSheet.create({
64 | wrapper: {
65 | flex: 1,
66 | alignItems: 'center',
67 | },
68 | image: {
69 | width: screenWidth - 20,
70 | height: 300,
71 | marginBottom: 5,
72 | },
73 | stepperContainer: {
74 | backgroundColor: 'transparent',
75 | flexDirection: 'row',
76 | borderWidth: 2,
77 | borderRadius: 8,
78 | overflow: 'hidden',
79 | alignItems: 'center',
80 | borderColor: '#ccc',
81 | },
82 | itemContainer: {
83 | marginBottom: 20,
84 | },
85 | smallItemContainer: {
86 | marginBottom: 5,
87 | },
88 | mainText: {
89 | fontSize: 20,
90 | },
91 | subText: {
92 | fontSize: 14,
93 | color: '#3a3a3a',
94 | },
95 | priceText: {
96 | fontSize: 40,
97 | fontWeight: 'bold',
98 | },
99 | labelText: {
100 | fontSize: 18,
101 | color: '#303540',
102 | },
103 | stepperButton: {
104 | height: 20,
105 | width: 20,
106 | },
107 | stepperText: {
108 | paddingLeft: 20,
109 | paddingRight: 20,
110 | fontSize: 20,
111 | fontWeight: 'bold',
112 | color: '#333',
113 | },
114 | });
115 |
116 | export default PageCard;
--------------------------------------------------------------------------------
/ordering-app/src/helpers/getSubTotal.js:
--------------------------------------------------------------------------------
1 | function sumofArray(sum, num) {
2 | return sum + num;
3 | }
4 |
5 | const getSubTotal = items => {
6 | if (items.length) {
7 | const subtotals = items.map(item => item.price * item.qty);
8 | return subtotals.reduce(sumofArray);
9 | }
10 | return 0;
11 | };
12 |
13 | export default getSubTotal;
14 |
--------------------------------------------------------------------------------
/ordering-app/src/helpers/location.js:
--------------------------------------------------------------------------------
1 | export function regionFrom(lat, lon, accuracy) {
2 | const oneDegreeOfLongitudeInMeters = 111.32 * 1000;
3 | const circumference = (40075 / 360) * 1000;
4 |
5 | const latDelta = accuracy * (1 / (Math.cos(lat) * circumference));
6 | const lonDelta = accuracy / oneDegreeOfLongitudeInMeters;
7 |
8 | return {
9 | latitude: lat,
10 | longitude: lon,
11 | latitudeDelta: Math.max(0, latDelta),
12 | longitudeDelta: Math.max(0, lonDelta),
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/ordering-app/src/screens/ContactDriver.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {View, StyleSheet} from 'react-native';
3 |
4 | import {GiftedChat} from 'react-native-gifted-chat';
5 | import {ChatManager, TokenProvider} from '@pusher/chatkit-client';
6 |
7 | import Config from 'react-native-config';
8 |
9 | const CHATKIT_INSTANCE_LOCATOR_ID = Config.CHATKIT_INSTANCE_LOCATOR_ID;
10 | const CHATKIT_SECRET_KEY = Config.CHATKIT_SECRET_KEY;
11 | const CHATKIT_TOKEN_PROVIDER_ENDPOINT = Config.CHATKIT_TOKEN_PROVIDER_ENDPOINT;
12 |
13 | import {AppContext} from '../../GlobalContext';
14 |
15 | class ContactDriver extends Component {
16 | static navigationOptions = ({navigation}) => {
17 | return {
18 | title: 'Contact Driver'
19 | };
20 | };
21 | //
22 |
23 | static contextType = AppContext;
24 |
25 | state = {
26 | messages: [],
27 | };
28 |
29 | async componentDidMount() {
30 | try {
31 | const chatManager = new ChatManager({
32 | instanceLocator: CHATKIT_INSTANCE_LOCATOR_ID,
33 | userId: this.context.user_id,
34 | tokenProvider: new TokenProvider({
35 | url: CHATKIT_TOKEN_PROVIDER_ENDPOINT,
36 | }),
37 | });
38 |
39 | let currentUser = await chatManager.connect();
40 | this.currentUser = currentUser;
41 |
42 | await this.currentUser.subscribeToRoomMultipart({
43 | roomId: this.context.room_id,
44 | hooks: {
45 | onMessage: this._onMessage,
46 | },
47 | messageLimit: 30,
48 | });
49 | } catch (err) {
50 | console.log('chatkit error: ', err);
51 | }
52 | }
53 |
54 | _onMessage = data => {
55 | const {message} = this._getMessage(data);
56 |
57 | this.setState(previousState => ({
58 | messages: GiftedChat.append(previousState.messages, message),
59 | }));
60 | };
61 |
62 | _getMessage = ({id, sender, parts, createdAt}) => {
63 | const text = parts.find(part => part.partType === 'inline').payload.content;
64 |
65 | const msg_data = {
66 | _id: id,
67 | text: text,
68 | createdAt: new Date(createdAt),
69 | user: {
70 | _id: sender.id.toString(),
71 | name: sender.name,
72 | avatar: `https://na.ui-avatars.com/api/?name=${sender.name}`,
73 | },
74 | };
75 |
76 | return {
77 | message: msg_data,
78 | };
79 | };
80 |
81 | render() {
82 | const {messages} = this.state;
83 |
84 | return (
85 |
86 | this._onSend(messages)}
89 | showUserAvatar={true}
90 | user={{
91 | _id: this.context.user_id,
92 | }}
93 | />
94 |
95 | );
96 | }
97 |
98 | _onSend = async ([message]) => {
99 | try {
100 | await this.currentUser.sendSimpleMessage({
101 | roomId: this.context.room_id,
102 | text: message.text,
103 | });
104 | } catch (send_msg_err) {
105 | console.log('error sending message: ', send_msg_err);
106 | }
107 | };
108 | }
109 |
110 | const styles = StyleSheet.create({
111 | wrapper: {
112 | flex: 1,
113 | },
114 | });
115 |
116 | export default ContactDriver;
--------------------------------------------------------------------------------
/ordering-app/src/screens/FoodDetails.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {View, Button, Alert} from 'react-native';
3 |
4 | import NavHeaderRight from '../components/NavHeaderRight';
5 | import PageCard from '../components/PageCard';
6 |
7 | import {AppContext} from '../../GlobalContext';
8 |
9 | class FoodDetails extends Component {
10 | static navigationOptions = ({navigation}) => {
11 | return {
12 | title: navigation.getParam('item').name.substr(0, 12) + '...',
13 | headerRight: (
14 |
15 | ),
16 | };
17 | };
18 |
19 | static contextType = AppContext;
20 |
21 | state = {
22 | qty: 1,
23 | };
24 |
25 | //
26 |
27 | constructor(props) {
28 | super(props);
29 | const {navigation} = this.props;
30 | this.item = navigation.getParam('item');
31 | }
32 |
33 | qtyChanged = value => {
34 | const nextValue = Number(value);
35 | this.setState({qty: nextValue});
36 | };
37 |
38 | addToCart = (item, qty) => {
39 | const item_id = this.context.cart_items.findIndex(
40 | el => el.restaurant.id !== item.restaurant.id,
41 | );
42 | if (item_id === -1) {
43 | Alert.alert(
44 | 'Added to basket',
45 | `${qty} ${item.name} was added to the basket.`,
46 | );
47 | this.context.addToCart(item, qty);
48 | } else {
49 | Alert.alert(
50 | 'Cannot add to basket',
51 | 'You can only order from one restaurant for each order.',
52 | );
53 | }
54 | };
55 |
56 | render() {
57 | const {qty} = this.state;
58 | return (
59 |
65 | );
66 | }
67 | }
68 | //
69 |
70 | export default FoodDetails;
71 |
--------------------------------------------------------------------------------
/ordering-app/src/screens/FoodList.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | View,
4 | Text,
5 | Button,
6 | TextInput,
7 | FlatList,
8 | StyleSheet,
9 | } from 'react-native';
10 | import axios from 'axios';
11 |
12 | import Config from 'react-native-config';
13 |
14 | import NavHeaderRight from '../components/NavHeaderRight';
15 | import ListCard from '../components/ListCard';
16 |
17 | const BASE_URL = Config.NGROK_HTTPS_URL;
18 |
19 | class FoodList extends Component {
20 | static navigationOptions = ({navigation}) => {
21 | return {
22 | title: 'Hungry?',
23 | headerRight: (
24 |
25 | ),
26 | };
27 | };
28 | //
29 |
30 | state = {
31 | foods: [],
32 | query: '',
33 | };
34 | //
35 |
36 | async componentDidMount() {
37 | try {
38 | const foods_response = await axios.get(`${BASE_URL}/foods`);
39 |
40 | this.setState({
41 | foods: foods_response.data.foods,
42 | });
43 | } catch (err) {
44 | console.log('err: ', err);
45 | }
46 | }
47 |
48 | onChangeQuery = text => {
49 | this.setState({
50 | query: text,
51 | });
52 | };
53 |
54 | render() {
55 | const {foods, query} = this.state;
56 | return (
57 |
58 |
59 |
60 |
66 |
67 |
68 |
69 |
75 |
76 |
77 | item.id.toString()}
82 | />
83 |
84 | );
85 | }
86 | //
87 |
88 | filterList = async () => {
89 | const {query} = this.state;
90 | const foods_response = await axios.get(`${BASE_URL}/foods?query=${query}`);
91 |
92 | this.setState({
93 | foods: foods_response.data.foods,
94 | query: '',
95 | });
96 | };
97 |
98 | viewItem = item => {
99 | this.props.navigation.navigate('FoodDetails', {
100 | item,
101 | });
102 | };
103 |
104 | renderFood = ({item}) => {
105 | return ;
106 | };
107 | }
108 | //
109 |
110 | const styles = StyleSheet.create({
111 | headerButtonContainer: {
112 | marginRight: 10,
113 | },
114 | wrapper: {
115 | flex: 1,
116 | padding: 10,
117 | },
118 | topWrapper: {
119 | flexDirection: 'row',
120 | },
121 | textInputWrapper: {
122 | flex: 4,
123 | },
124 | textInput: {
125 | height: 35,
126 | borderColor: '#5d5d5d',
127 | borderWidth: 1,
128 | },
129 | buttonWrapper: {
130 | flex: 1,
131 | },
132 | list: {
133 | marginTop: 20,
134 | },
135 | });
136 |
137 | export default FoodList;
--------------------------------------------------------------------------------
/ordering-app/src/screens/OrderSummary.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | View,
4 | Text,
5 | Button,
6 | TouchableOpacity,
7 | FlatList,
8 | StyleSheet,
9 | } from 'react-native';
10 | import MapView from 'react-native-maps';
11 | import RNGooglePlaces from 'react-native-google-places';
12 | import {check, request, PERMISSIONS, RESULTS} from 'react-native-permissions';
13 |
14 | import Geolocation from 'react-native-geolocation-service';
15 | import Geocoder from 'react-native-geocoding';
16 | import Config from 'react-native-config';
17 |
18 | import {AppContext} from '../../GlobalContext';
19 |
20 | import getSubTotal from '../helpers/getSubTotal';
21 |
22 | import {regionFrom} from '../helpers/location';
23 |
24 | const GOOGLE_API_KEY = Config.GOOGLE_API_KEY;
25 |
26 | Geocoder.init(GOOGLE_API_KEY);
27 |
28 | const random = require('string-random');
29 | import axios from 'axios';
30 |
31 | const BASE_URL = Config.NGROK_HTTPS_URL;
32 |
33 | class OrderSummary extends Component {
34 | static navigationOptions = {
35 | title: 'Order Summary',
36 | };
37 |
38 | static contextType = AppContext;
39 |
40 | state = {
41 | customer_address: '',
42 | customer_location: null,
43 | restaurant_address: '',
44 | restaurant_location: null,
45 | };
46 |
47 | async componentDidMount() {
48 | let location_permission = await check(
49 | PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
50 | );
51 |
52 | if (location_permission === 'denied') {
53 | location_permission = await request(
54 | PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
55 | );
56 | }
57 |
58 | if (location_permission == 'granted') {
59 | Geolocation.getCurrentPosition(
60 | async position => {
61 | const geocoded_location = await Geocoder.from(
62 | position.coords.latitude,
63 | position.coords.longitude,
64 | );
65 |
66 | let customer_location = regionFrom(
67 | position.coords.latitude,
68 | position.coords.longitude,
69 | position.coords.accuracy,
70 | );
71 |
72 | this.setState({
73 | customer_address: geocoded_location.results[0].formatted_address,
74 | customer_location,
75 | });
76 | },
77 | error => {
78 | console.log(error.code, error.message);
79 | },
80 | {
81 | enableHighAccuracy: true,
82 | timeout: 15000,
83 | maximumAge: 10000,
84 | },
85 | );
86 | }
87 | }
88 |
89 | openPlacesSearchModal = async () => {
90 | try {
91 | const place = await RNGooglePlaces.openAutocompleteModal();
92 |
93 | const customer_location = regionFrom(
94 | place.location.latitude,
95 | place.location.longitude,
96 | 16,
97 | );
98 |
99 | this.setState({
100 | customer_address: place.address,
101 | customer_location,
102 | });
103 | } catch (err) {
104 | console.log('err: ', err);
105 | }
106 | };
107 |
108 | renderAddressParts = customer_address => {
109 | return customer_address.split(',').map((addr_part, index) => {
110 | return (
111 |
112 | {addr_part}
113 |
114 | );
115 | });
116 | };
117 | //
118 |
119 | render() {
120 | const subtotal = getSubTotal(this.context.cart_items);
121 | const {customer_address, customer_location} = this.state;
122 |
123 | return (
124 |
125 |
126 | {customer_location && (
127 |
128 |
129 |
130 | )}
131 |
132 |
133 | {customer_address != '' &&
134 | this.renderAddressParts(customer_address)}
135 |
136 | {
138 | this.openPlacesSearchModal();
139 | }}>
140 |
141 | Change location
142 |
143 |
144 |
145 |
146 |
147 | item.id.toString()}
151 | />
152 |
153 |
154 |
155 |
156 |
157 | {subtotal > 0 && (
158 |
159 |
160 | Subtotal
161 | Booking fee
162 | Total
163 |
164 |
165 |
166 | ${subtotal}
167 | $5
168 | ${subtotal + 5}
169 |
170 |
171 | )}
172 |
173 |
174 | {subtotal == 0 && (
175 |
176 | Your cart is empty
177 |
178 | )}
179 |
180 | {subtotal > 0 && (
181 |
182 |
188 | )}
189 |
190 | );
191 | }
192 | //
193 |
194 | placeOrder = async () => {
195 | const {customer_location, customer_address} = this.state;
196 |
197 | const room_id = random();
198 | const room_name = `Order ${room_id}`;
199 |
200 | this.context.setRoom(room_id, room_name);
201 |
202 | const {
203 | address: restaurant_address,
204 | location: restaurant_location,
205 | } = this.context.cart_items[0].restaurant;
206 |
207 | try {
208 | // login chatkit user
209 | await axios.post(`${BASE_URL}/login`, {
210 | user_id: this.context.user_id,
211 | user_name: this.context.user_name,
212 | user_type: this.context.user_type,
213 | });
214 |
215 | await axios.post(`${BASE_URL}/room`, {
216 | room_id,
217 | room_name: room_name,
218 | user_id: this.context.user_id,
219 | });
220 | } catch (err) {
221 | console.log('login err: ', err);
222 | }
223 |
224 | this.props.navigation.navigate('TrackOrder', {
225 | customer_location,
226 | restaurant_location,
227 | customer_address,
228 | restaurant_address,
229 | });
230 | };
231 |
232 | renderCartItem = ({item}) => {
233 | return (
234 |
235 |
236 |
237 | {item.qty}x {item.name}
238 |
239 |
240 |
241 | ${item.price}
242 |
243 |
244 | );
245 | };
246 | }
247 | //
248 |
249 | const styles = StyleSheet.create({
250 | wrapper: {
251 | flex: 1,
252 | },
253 | addressSummaryContainer: {
254 | flex: 2,
255 | flexDirection: 'row',
256 | },
257 | addressContainer: {
258 | padding: 10,
259 | },
260 | mapContainer: {
261 | width: 125,
262 | height: 125,
263 | },
264 | map: {
265 | ...StyleSheet.absoluteFillObject,
266 | },
267 | addressText: {
268 | fontSize: 16,
269 | },
270 | linkButtonContainer: {
271 | marginTop: 10,
272 | },
273 | linkButton: {
274 | color: '#0366d6',
275 | fontSize: 13,
276 | textDecorationLine: 'underline',
277 | },
278 | cartItemsContainer: {
279 | flex: 5,
280 | marginTop: 20,
281 | },
282 | lowerContainer: {
283 | flex: 1,
284 | flexDirection: 'row',
285 | },
286 | spacerBox: {
287 | flex: 2,
288 | },
289 | cartItemContainer: {
290 | flexDirection: 'row',
291 | justifyContent: 'space-between',
292 | padding: 10,
293 | },
294 | paymentSummaryContainer: {
295 | flex: 2,
296 | flexDirection: 'row',
297 | justifyContent: 'space-between',
298 | marginRight: 20,
299 | },
300 | endLabelContainer: {
301 | alignItems: 'flex-end',
302 | },
303 | price: {
304 | fontSize: 17,
305 | fontWeight: 'bold',
306 | },
307 | priceLabel: {
308 | fontSize: 16,
309 | },
310 | messageBox: {
311 | flex: 1,
312 | alignItems: 'center',
313 | justifyContent: 'center',
314 | backgroundColor: '#4c90d4',
315 | },
316 | messageBoxText: {
317 | fontSize: 18,
318 | color: '#fff',
319 | },
320 | buttonContainer: {
321 | flex: 1,
322 | padding: 20,
323 | },
324 | });
325 |
326 | export default OrderSummary;
327 |
--------------------------------------------------------------------------------
/ordering-app/src/screens/TrackOrder.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {View, Text, Button, Alert, StyleSheet} from 'react-native';
3 |
4 | import MapView from 'react-native-maps';
5 | import Geolocation from 'react-native-geolocation-service';
6 | import MapViewDirections from 'react-native-maps-directions';
7 | import Pusher from 'pusher-js/react-native';
8 |
9 | import Config from 'react-native-config';
10 |
11 | import RNPusherPushNotifications from 'react-native-pusher-push-notifications';
12 | import axios from 'axios';
13 |
14 | const CHANNELS_APP_KEY = Config.CHANNELS_APP_KEY;
15 | const CHANNELS_APP_CLUSTER = Config.CHANNELS_APP_CLUSTER;
16 | const BASE_URL = Config.NGROK_HTTPS_URL;
17 |
18 | const GOOGLE_API_KEY = Config.GOOGLE_API_KEY;
19 |
20 | import {regionFrom} from '../helpers/location';
21 |
22 | import {AppContext} from '../../GlobalContext';
23 |
24 | const orderSteps = [
25 | 'Finding a driver',
26 | 'Driver is on the way to pick up your order',
27 | 'Driver has picked up your order and is on the way to deliver it',
28 | 'Driver has delivered your order',
29 | ];
30 |
31 | RNPusherPushNotifications.setInstanceId(Config.BEAMS_INSTANCE_ID);
32 |
33 | const subscribeToRoom = room_id => {
34 | RNPusherPushNotifications.subscribe(
35 | room_id,
36 | (statusCode, response) => {
37 | console.error(statusCode, response);
38 | },
39 | () => {
40 | console.log('Success');
41 | },
42 | );
43 | };
44 |
45 | class TrackOrder extends Component {
46 | static navigationOptions = ({navigation}) => {
47 | return {
48 | title: 'Track Order',
49 | };
50 | };
51 |
52 | static contextType = AppContext;
53 |
54 | state = {
55 | isSearching: true,
56 | hasDriver: false,
57 | driverLocation: null,
58 | orderStatusText: orderSteps[0],
59 | };
60 |
61 | constructor(props) {
62 | super(props);
63 |
64 | this.customer_location = this.props.navigation.getParam(
65 | 'customer_location',
66 | ); // customer's location
67 | this.restaurant_location = this.props.navigation.getParam(
68 | 'restaurant_location',
69 | );
70 |
71 | this.customer_address = this.props.navigation.getParam('customer_address');
72 | this.restaurant_address = this.props.navigation.getParam(
73 | 'restaurant_address',
74 | );
75 |
76 | this.available_drivers_channel = null; // the pusher channel where all drivers and customers are subscribed to
77 | this.user_ride_channel = null; // the pusher channel exclusive to the customer and driver in a given ride
78 | this.pusher = null;
79 | }
80 |
81 | componentDidMount() {
82 | this.setState({
83 | isSearching: true, // show the loader
84 | });
85 |
86 | this.pusher = new Pusher(CHANNELS_APP_KEY, {
87 | authEndpoint: `${BASE_URL}/pusher/auth`,
88 | cluster: CHANNELS_APP_CLUSTER,
89 | encrypted: true,
90 | });
91 |
92 | this.available_drivers_channel = this.pusher.subscribe(
93 | 'private-available-drivers',
94 | );
95 |
96 | this.available_drivers_channel.bind('pusher:subscription_succeeded', () => {
97 | // make a request to all drivers
98 | setTimeout(() => {
99 | this.available_drivers_channel.trigger('client-driver-request', {
100 | customer: {username: this.context.user_id},
101 | restaurant_location: this.restaurant_location,
102 | customer_location: this.customer_location,
103 | restaurant_address: this.restaurant_address,
104 | customer_address: this.customer_address,
105 | });
106 | }, 2000);
107 | });
108 |
109 | this.user_ride_channel = this.pusher.subscribe(
110 | 'private-ride-' + this.context.user_id,
111 | );
112 |
113 | this.user_ride_channel.bind('client-driver-response', data => {
114 | // customer responds to driver's response
115 | const {hasDriver} = this.state;
116 | this.user_ride_channel.trigger('client-driver-response', {
117 | response: hasDriver ? 'no' : 'yes',
118 | room_id: hasDriver ? '0' : this.context.room_id,
119 | room_name: hasDriver ? '' : this.context.room_name,
120 | });
121 |
122 | if (!hasDriver) {
123 | setTimeout(async () => {
124 | const res = await axios.post(
125 | `${BASE_URL}/push/${this.context.room_id}`,
126 | {
127 | push_type: 'customer_confirmed',
128 | data: this.context.user_name,
129 | },
130 | );
131 | }, 5000);
132 | }
133 | });
134 |
135 | this.user_ride_channel.bind('client-found-driver', data => {
136 | // found driver, the customer has no say about this.
137 | const driverLocation = regionFrom(
138 | data.location.latitude,
139 | data.location.longitude,
140 | data.location.accuracy,
141 | );
142 |
143 | this.setState({
144 | hasDriver: true,
145 | isSearching: false,
146 | driverLocation,
147 | });
148 |
149 | Alert.alert(
150 | 'Driver found',
151 | "We found you a driver. They're on their way to pick up your order.",
152 | );
153 | });
154 |
155 | this.user_ride_channel.bind('client-driver-location', data => {
156 | // driver location received
157 | let driverLocation = regionFrom(
158 | data.latitude,
159 | data.longitude,
160 | data.accuracy,
161 | );
162 |
163 | this.setState({
164 | driverLocation,
165 | });
166 | });
167 |
168 | this.user_ride_channel.bind('client-order-update', data => {
169 | this.setState({
170 | orderStatusText: orderSteps[data.step],
171 | });
172 | });
173 |
174 | subscribeToRoom(this.context.room_id);
175 |
176 | RNPusherPushNotifications.on('notification', noty => {
177 | Alert.alert(noty.title, noty.body);
178 | });
179 | }
180 |
181 | contactDriver = () => {
182 | this.props.navigation.navigate('ContactDriver');
183 | };
184 |
185 | render() {
186 | const {driverLocation, orderStatusText} = this.state;
187 |
188 | return (
189 |
190 |
191 | {orderStatusText}
192 |
193 |
199 |
200 |
201 |
205 |
212 |
213 | {driverLocation && (
214 |
219 | )}
220 |
221 |
229 |
230 | {driverLocation && (
231 |
241 | )}
242 |
243 |
256 |
257 |
258 |
259 | );
260 | }
261 | }
262 | //
263 |
264 | const styles = StyleSheet.create({
265 | wrapper: {
266 | flex: 1,
267 | },
268 | infoContainer: {
269 | flex: 1,
270 | padding: 20,
271 | },
272 | infoText: {
273 | marginBottom: 10,
274 | },
275 | mapContainer: {
276 | flex: 9,
277 | },
278 | map: {
279 | ...StyleSheet.absoluteFillObject,
280 | },
281 | });
282 |
283 | export default TrackOrder;
284 |
--------------------------------------------------------------------------------
/server/.env:
--------------------------------------------------------------------------------
1 | PUSHER_APP_ID="YOUR PUSHER APP ID"
2 | PUSHER_APP_KEY="YOUR PUSHER APP KEY"
3 | PUSHER_APP_SECRET="YOUR PUSHER APP SECRET"
4 | PUSHER_APP_CLUSTER="YOUR PUSHER APP CLUSTER"
5 |
6 | CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT INSTANCE LOCATOR ID"
7 | CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET KEY"
8 |
9 | CHATKIT_WEBHOOK_SECRET="YOUR CHATKIT WEBHOOK SECRET"
10 |
11 | BEAMS_INSTANCE_ID_DRIVER="YOUR BEAMS INSTANCE ID FOR THE DRIVER APP"
12 | BEAMS_SECRET_KEY_DRIVER="YOUR BEAMS SECRET KEY FOR THE DRIVER APP"
13 |
14 | BEAMS_INSTANCE_ID_CUSTOMER="YOUR BEAMS INSTANCE ID FOR THE ORDERING APP"
15 | BEAMS_SECRET_KEY_CUSTOMER="YOUR BEAMS SECRET KEY FOR THE ORDERING APP"
--------------------------------------------------------------------------------
/server/data/foods.js:
--------------------------------------------------------------------------------
1 | const foods_r = [
2 | {
3 | id: 1,
4 | name: 'Spicy Teriyaki',
5 | price: 19.25,
6 | image: 'spicy-teriyaki.jpg',
7 |
8 | restaurant: {
9 | id: 25,
10 | name: 'MIZ Japanese Restaurant',
11 | address: '17 Kampong Bahru Rd, Singapore 169347',
12 | location: [16.618037, 120.3146543],
13 | },
14 | },
15 | {
16 | id: 2,
17 | name: 'Honey Garlic Chicken',
18 | price: 5.5,
19 | image: 'honey-garlic-chicken.jpg',
20 | restaurant_id: 26,
21 |
22 | restaurant: {
23 | id: 26,
24 | name: 'Everton Food Place',
25 | address: '7 Everton Park, Singapore 080007',
26 | location: [1.2773164, 103.8384773],
27 | },
28 | },
29 | {
30 | id: 3,
31 | name: 'La-La White Bee Hoon',
32 | price: 15.94,
33 | image: 'white-bee-hoon.jpg',
34 |
35 | restaurant: {
36 | id: 27,
37 | name: 'Botany Robertson Quay',
38 | address: '86, #01-03 Robertson Quay, Singapore 238245',
39 | location: [1.2900394, 103.8351518],
40 | },
41 | },
42 | {
43 | id: 4,
44 | name: 'Sesame Chicken Noodle',
45 | price: 15.94,
46 | image: 'sesame-chicken-noodle.jpg',
47 | restaurant_id: 28,
48 | restaurant: 'Hai Tien Lo',
49 | location: [1.292396, 103.8562925],
50 | },
51 | {
52 | id: 5,
53 | name: 'Fried Mee Sua with Shrimps and Scallop',
54 | price: 15.94,
55 | image: 'fried-mee-sua.jpg',
56 |
57 | restaurant: {
58 | id: 29,
59 | name: 'Imperial Treasure Super Peking Duck',
60 | address: '7 Raffles Blvd, Singapore 039595',
61 | location: [1.3033948, 103.833346],
62 | },
63 | },
64 | {
65 | id: 6,
66 | name: 'Pork with Vegetables',
67 | price: 10.5,
68 | image: 'pork-with-veggies.jpg',
69 |
70 | restaurant: {
71 | id: 30,
72 | name: 'Pek Kio Market & Food Centre',
73 | address: '41 Cambridge Rd, Singapore 210041',
74 | location: [1.3161213, 103.8480768],
75 | },
76 | },
77 | {
78 | id: 7,
79 | name: 'BBQ Red Pork with Egg Noodles',
80 | price: 12.9,
81 | image: 'red-bbq-pork-noodles.jpg',
82 |
83 | restaurant: {
84 | id: 31,
85 | name: 'Kim Keat Hokkien Mee',
86 | address: '92 Lor 4 Toa Payoh, Singapore 310092',
87 | location: [1.3380931, 103.8490967],
88 | },
89 | },
90 | {
91 | id: 8,
92 | name: 'Vietnamese Pho',
93 | price: 70,
94 | image: 'vietnamese-pho.jpg',
95 |
96 | restaurant: {
97 | id: 32,
98 | name: 'Mrs Pho',
99 | address: '221 Rangoon Rd, Singapore 218459',
100 | location: [1.2643737, 103.8201297],
101 | },
102 | },
103 | {
104 | id: 9,
105 | name: 'Rice with Roasted Pork',
106 | price: 20.1,
107 | image: 'rice-with-roasted-pork.jpg',
108 |
109 | restaurant: {
110 | id: 34,
111 | name: 'Seah Im Food Centre',
112 | address: '2 Seah Im Rd, Singapore 099114',
113 | location: [1.2664712, 103.8173126],
114 | },
115 | },
116 | {
117 | id: 10,
118 | name: 'Tori Karaage',
119 | price: 30,
120 | image: 'tori-karaage.jpg',
121 |
122 | restaurant: {
123 | id: 35,
124 | name: 'Tatsuya',
125 | address: '22 Scotts Rd, Goodwood Park Hotel, Singapore 228221',
126 | location: [1.3084636, 103.8317653],
127 | },
128 | },
129 | {
130 | id: 11,
131 | name: 'Salmon Sashimi',
132 | price: 80,
133 | image: 'salmon-sashimi.jpg',
134 |
135 | restaurant: {
136 | id: 35,
137 | name: 'Tatsuya',
138 | address: '22 Scotts Rd, Goodwood Park Hotel, Singapore 228221',
139 | location: [1.3084636, 103.8317653],
140 | },
141 | },
142 | {
143 | id: 12,
144 | name: 'Gyoza',
145 | price: 40.99,
146 | image: 'gyoza.jpg',
147 |
148 | restaurant: {
149 | id: 35,
150 | name: 'Tatsuya',
151 | address: '22 Scotts Rd, Goodwood Park Hotel, Singapore 228221',
152 | location: [1.3084636, 103.8317653],
153 | },
154 | },
155 | ];
156 |
157 | module.exports.foods = () => {
158 | return foods_r;
159 | };
160 |
--------------------------------------------------------------------------------
/server/images/fried-mee-sua.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/fried-mee-sua.jpg
--------------------------------------------------------------------------------
/server/images/gyoza.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/gyoza.jpg
--------------------------------------------------------------------------------
/server/images/honey-garlic-chicken.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/honey-garlic-chicken.jpg
--------------------------------------------------------------------------------
/server/images/pork-with-veggies.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/pork-with-veggies.jpg
--------------------------------------------------------------------------------
/server/images/red-bbq-pork-noodles.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/red-bbq-pork-noodles.jpg
--------------------------------------------------------------------------------
/server/images/rice-with-roasted-pork.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/rice-with-roasted-pork.jpg
--------------------------------------------------------------------------------
/server/images/salmon-sashimi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/salmon-sashimi.jpg
--------------------------------------------------------------------------------
/server/images/sesame-chicken-noodle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/sesame-chicken-noodle.jpg
--------------------------------------------------------------------------------
/server/images/spicy-teriyaki.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/spicy-teriyaki.jpg
--------------------------------------------------------------------------------
/server/images/tori-karaage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/tori-karaage.jpg
--------------------------------------------------------------------------------
/server/images/vietnamese-pho.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/vietnamese-pho.jpg
--------------------------------------------------------------------------------
/server/images/white-bee-hoon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anchetaWern/React-Native-Food-Delivery/933450a91409de7f92800ade492c70f356507278/server/images/white-bee-hoon.jpg
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 | const cors = require('cors');
4 | const Chatkit = require('@pusher/chatkit-server');
5 | const PushNotifications = require('@pusher/push-notifications-server');
6 | require('dotenv').config();
7 |
8 | const crypto = require('crypto');
9 |
10 | const Pusher = require('pusher');
11 |
12 | var pusher = new Pusher({
13 | // connect to pusher
14 | appId: process.env.PUSHER_APP_ID,
15 | key: process.env.PUSHER_APP_KEY,
16 | secret: process.env.PUSHER_APP_SECRET,
17 | cluster: process.env.PUSHER_APP_CLUSTER,
18 | });
19 |
20 | const {foods} = require('./data/foods.js');
21 |
22 | const app = express();
23 |
24 | const CHATKIT_INSTANCE_LOCATOR_ID = process.env.CHATKIT_INSTANCE_LOCATOR_ID;
25 | const CHATKIT_SECRET_KEY = process.env.CHATKIT_SECRET_KEY;
26 |
27 | const chatkit = new Chatkit.default({
28 | instanceLocator: CHATKIT_INSTANCE_LOCATOR_ID,
29 | key: CHATKIT_SECRET_KEY,
30 | });
31 |
32 | const CHATKIT_WEBHOOK_SECRET = process.env.CHATKIT_WEBHOOK_SECRET;
33 |
34 | const beamsClientDriver = new PushNotifications({
35 | instanceId: process.env.BEAMS_INSTANCE_ID_DRIVER,
36 | secretKey: process.env.BEAMS_SECRET_KEY_DRIVER,
37 | });
38 |
39 | const beamsClientCustomer = new PushNotifications({
40 | instanceId: process.env.BEAMS_INSTANCE_ID_CUSTOMER,
41 | secretKey: process.env.BEAMS_SECRET_KEY_CUSTOMER,
42 | });
43 |
44 | const push_types = {
45 | driver_accepted_order: {
46 | title: 'Order accepted',
47 | body: '[data] has accepted your order',
48 | },
49 | driver_picked_order: {
50 | title: 'Picked up order',
51 | body: '[data] has picked up your order from the restaurant',
52 | },
53 | driver_delivered_order: {
54 | title: 'Order delivered',
55 | body: '[data] has delivered your order',
56 | },
57 | driver_sent_message: {
58 | title: 'New message',
59 | body: '[data]',
60 | },
61 |
62 | customer_confirmed: {
63 | title: 'Customer confirmed',
64 | body: '[data] has confirmed',
65 | },
66 | customer_sent_message: {
67 | title: 'New message',
68 | body: '[data]',
69 | },
70 | };
71 |
72 | app.use(
73 | bodyParser.text({
74 | type: req => {
75 | const contype = req.headers['content-type'];
76 | if (contype === 'application/json') {
77 | return true;
78 | }
79 | return false;
80 | },
81 | }),
82 | );
83 |
84 | app.use(
85 | bodyParser.json({
86 | type: req => {
87 | const contype = req.headers['content-type'];
88 | if (contype !== 'application/json') {
89 | return true;
90 | }
91 | return false;
92 | },
93 | }),
94 | );
95 |
96 | app.use(bodyParser.urlencoded({extended: false}));
97 |
98 | app.use(cors());
99 | app.use('/images', express.static('images'));
100 |
101 | const verifyRequest = req => {
102 | const signature = crypto
103 | .createHmac('sha1', CHATKIT_WEBHOOK_SECRET)
104 | .update(req.body)
105 | .digest('hex');
106 |
107 | return signature === req.get('webhook-signature');
108 | };
109 |
110 | const getUser = async user_id => {
111 | try {
112 | const user = await chatkit.getUser({
113 | id: user_id,
114 | });
115 | return user;
116 | } catch (err) {
117 | console.log('error getting user: ', err);
118 | return false;
119 | }
120 | };
121 |
122 | const publishNotification = async (user_type, order_id, title, body) => {
123 | const beamsClient =
124 | user_type == 'driver' ? beamsClientCustomer : beamsClientDriver;
125 |
126 | try {
127 | await beamsClient.publishToInterests([order_id], {
128 | fcm: {
129 | notification: {
130 | title,
131 | body,
132 | },
133 | },
134 | });
135 | } catch (err) {
136 | console.log('error publishing push notification: ', err);
137 | }
138 | };
139 |
140 | const notifyUser = async ({payload}) => {
141 | try {
142 | const msg = payload.messages[0];
143 | const sender_id = msg.user_id;
144 | const sender = await getUser(sender_id);
145 |
146 | const message = msg.parts[0].content.substr(0, 37) + '...';
147 | const order_id = msg.room_id;
148 |
149 | const user_type = sender.custom_data.user_type;
150 |
151 | const push_data = push_types[`${user_type}_sent_message`];
152 | const title = push_data.title;
153 | const body = push_data.body.replace('[data]', message);
154 |
155 | await publishNotification(user_type, order_id, title, body);
156 | } catch (err) {
157 | console.log('notify user err: ', err);
158 | }
159 | };
160 |
161 | app.post('/pusher/auth', function(req, res) {
162 | var socketId = req.body.socket_id;
163 | var channel = req.body.channel_name;
164 | var auth = pusher.authenticate(socketId, channel);
165 | res.send(auth);
166 | });
167 |
168 | app.get('/foods/:query?', (req, res) => {
169 | const foods_r = foods();
170 |
171 | if (req.query.query != undefined) {
172 | const query = req.query.query.toLowerCase();
173 | return res.send({
174 | foods: foods_r.filter(
175 | itm =>
176 | itm.name.toLowerCase().includes(query) ||
177 | itm.restaurant.toLowerCase().includes(query),
178 | ),
179 | });
180 | }
181 |
182 | return res.send({
183 | foods: foods_r,
184 | });
185 | });
186 |
187 | app.post('/login', async (req, res) => {
188 | const {user_id, user_name, user_type} = req.body;
189 | const user = await getUser(user_id);
190 |
191 | if (!user) {
192 | await chatkit.createUser({
193 | id: user_id,
194 | name: user_name,
195 | customData: {
196 | user_type,
197 | },
198 | });
199 | }
200 |
201 | return res.send('ok');
202 | });
203 |
204 | app.post('/room', async (req, res) => {
205 | const {room_id, room_name, user_id} = req.body;
206 |
207 | try {
208 | const room = await chatkit.getRoom({
209 | roomId: room_id,
210 | includePrivate: true,
211 | });
212 |
213 | if (room) {
214 | const user_rooms = await chatkit.getUserRooms({
215 | userId: user_id,
216 | });
217 |
218 | const room_index = user_rooms.findIndex(item => item.id == room_id);
219 | if (room_index == -1) {
220 | const add_user_to_room = await chatkit.addUsersToRoom({
221 | roomId: room_id,
222 | userIds: [user_id],
223 | });
224 | }
225 | }
226 | } catch (err) {
227 | if (err.error == 'services/chatkit/not_found/room_not_found') {
228 | await chatkit.createRoom({
229 | id: room_id,
230 | creatorId: user_id,
231 | name: room_name,
232 | isPrivate: true,
233 | });
234 | }
235 | }
236 |
237 | return res.send('ok');
238 | });
239 |
240 | app.post('/push/:order_id', async (req, res) => {
241 | const {data, push_type} = req.body;
242 | const {order_id} = req.params;
243 |
244 | const user_type = push_type.split('_')[0];
245 |
246 | const push_data = push_types[push_type];
247 | const title = push_data.title;
248 | const body = push_data.body.replace('[data]', data);
249 |
250 | await publishNotification(user_type, order_id, title, body);
251 |
252 | return res.send('ok');
253 | });
254 |
255 | app.post('/notify', (req, res) => {
256 | if (verifyRequest(req)) {
257 | const data = JSON.parse(req.body);
258 | const type = data.metadata.event_type;
259 | if (type == 'v1.messages_created') {
260 | notifyUser(data);
261 | }
262 | res.sendStatus(200);
263 | } else {
264 | console.log('Unverified request');
265 | res.sendStatus(401); // unauthorized
266 | }
267 | });
268 |
269 | const PORT = 5000;
270 | app.listen(PORT, err => {
271 | if (err) {
272 | console.error(err);
273 | } else {
274 | console.log(`Running on ports ${PORT}`);
275 | }
276 | });
277 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "food-delivery-server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node index.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "@pusher/chatkit-server": "^2.1.1",
14 | "@pusher/push-notifications-server": "^1.2.0",
15 | "body-parser": "^1.18.3",
16 | "cors": "^2.8.5",
17 | "dotenv": "^8.1.0",
18 | "express": "4.17.1",
19 | "pusher": "^3.0.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------