Click here to Skip to main content
15,569,845 members
Articles / Web Development / Node.js
Article
Posted 10 Nov 2022

Stats

29.5K views
1.4K downloads
72 bookmarked

BookCars - Car Rental Platform with Mobile App

Rate me:
Please Sign up or sign in to vote.
4.97/5 (33 votes)
31 Jan 2023MIT14 min read
Car Rental Platform with a mobile app
In this article, you will learn about BookCars, a car rental platform with mobile app.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Quick Overview
    1. Frontend
    2. Mobile App
    3. Backend
  4. Background
  5. Using the Code
    1. API
    2. Frontend
    3. Mobile App
    4. Backend
  6. Installation
  7. Change Currency
  8. Demo Database
  9. Run from Code
  10. Run Mobile App
  11. Points of Interest
  12. History

Introduction

Image 1

BookCars is a car rental platform, supplier oriented, with a backend for managing car fleet and bookings, a frontend and a mobile app for car rental.

BookCars provides the following features:

  • Supplier management
  • Car fleet management
  • Booking management
  • Client management
  • Multiple payment methods
  • Multiple language support
  • Responsive backend and frontend
  • Native Mobile app for Android and iOS

In this article, you will learn how BookCars was made including a description of the main parts of the source code and the software architecture, how to deploy it, and how to run the source code. But before we dig in, we'll start with a quick overview of the platform.

Prerequisites

  • Node.js
  • Express
  • MongoDB
  • React
  • MUI
  • React Native
  • Expo
  • JavaScript
  • Git

Quick Overview

In this section, you'll see a quick overview of the main pages of the frontend, the backend and the mobile app.

Frontend

From the frontend, the user can search for available cars, choose a car and checkout.

Below is the main page of the frontend where the user can choose a pickup and drop-off locations, dates and time booking, and search for available cars.

Image 2

Below is the search result of the main page where the user can choose a car for rental.

Image 3

Below is the checkout page where the user can set rental options and checkout. If the user is not registered, he can checkout and register at the same time. He will receive a confirmation and activation email to set his password if he is not registered yet.

Image 4

Below is the sign in page.

Image 5

Below is the sign up page.

Image 6

That's it! That's the main pages of the frontend.

Mobile App

From the mobile app, the user can search for available cars, choose a car and checkout.

The user can also receive push notifications, if the status of his booking is updated.

Below are the main pages of the mobile app where the user can choose pickup and drop-off locations, date and time booking, and search for available cars.

Image 7 Image 8 Image 9

Below is the search result of the main page where user can choose a car for rental and checkout.

Image 10 Image 11 Image 12 Image 13

Below are sign in and sign up pages.

Image 14 Image 15 Image 16

Below are the pages where the user can see and manage his bookings.

Image 17 Image 18 Image 19 Image 20 Image 21

Below are the pages where the user can update his profile information, change his password and see his notifications.

Image 22 Image 23 Image 24 Image 25

That's it for the main pages of the mobile app.

Backend

BookCars is supplier oriented. This means that there are three types of users:

  • Admin: He has full access to the backend. He can do everything.
  • Supplier: He has restricted access to the backend. He can only manage his cars and bookings.
  • User: He has access only to the frontend and the mobile app. He cannot access the backend.

BookCars is designed to work with multiple suppliers. Each supplier can manage his car fleet and bookings from the backend. BookCars can also work with only one supplier too.

From the backend, the admin user can create and manage suppliers, cars, locations, users and bookings.

When the admin user creates a new supplier, the supplier will receive an automatic email for creating his account to access the backend so he can manage his car fleet and bookings.

Below is the sign in page of the backend.

Image 26

Below is the dashboard page of the backend where admins and suppliers can see and manage bookings.

Image 27

Below is the page where car fleet is displayed and can be managed.

Image 28

Below is the page where admins and suppliers can create new cars by providing an image and car info.

Image 29

Below is the page where admins and suppliers can edit cars.

Image 30

Below is the page where admins can manage platform users.

Image 31

Below is the page where to edit bookings.

Image 32

That's it! That's the main pages of the backend.

Background

The basic idea behind BookCars is very simple:

  • A backend: From which admins create new suppliers. Each supplier will receive an automatic email to activate his account and have access to the backend so he can manage his car fleet and bookings.
  • A frontend and a mobile app: From which users can see available cars depending on some parameters such as pickup and drop-off locations, and date and time booking. Then, users can proceed to checkout in order to book their cars.

The backend, the frontend and the mobile app rely on BookCars API which is a RESTful API that exposes functions to access BookCars database.

Using the Code

Image 33

This section describes the software architecture of BookCars including the API, the frontend, the mobile app and the backend.

BookCars API is a Node.js server application that exposes a RESTful API using Express which gives access to BookCars MongoDB database.

BookCars frontend is a React web application that is the main web interface for booking cars.

Bookcars backend is a React web application that lets admins and suppliers manage car fleets and bookings.

BookCars mobile app is a React Native application that is the main mobile app for booking cars.

API

BookCars API exposes all BookCars functions needed for the backend, the frontend and the mobile app. The API follows the MVC design pattern. JWT is used for authentication. There are some functions that need authentication such as functions related to managing cars and bookings and others that do not need authentication such as retrieving locations and available cars for non authenticated users.

Image 34

  • ./api/models/ folder contains MongoDB models.
  • ./api/routes/ folder contains Express routes.
  • ./api/controllers/ folder contains controllers.
  • ./api/middlewares/ folder contains middlewares.
  • ./api/server.js is the main server where database connection is established and routes are loaded.
  • ./api/app.js is the main entry point of BookCars API.

app.js

app.js is the main entry point of BookCars API:

JavaScript
import app from './server.js'
import fs from 'fs'
import https from 'https'

const PORT = parseInt(process.env.BC_PORT) || 4000
const HTTPS = process.env.BC_HTTPS.toLocaleLowerCase() === 'true'
const PRIVATE_KEY = process.env.BC_PRIVATE_KEY
const CERTIFICATE = process.env.BC_CERTIFICATE

if (HTTPS) {
    https.globalAgent.maxSockets = Infinity
    const privateKey = fs.readFileSync(PRIVATE_KEY, 'utf8')
    const certificate = fs.readFileSync(CERTIFICATE, 'utf8')
    const credentials = { key: privateKey, cert: certificate }
    const httpsServer = https.createServer(credentials, app)

    httpsServer.listen(PORT, () => {
        console.log('HTTPS server is running on Port:', PORT)
    })
} else {
    app.listen(PORT, () => {
        console.log('HTTP server is running on Port:', PORT)
    })
}

In app.js, we retrieve HTTPS setting variable which indicate if https is enabled or not. If https is enabled, we create an https server using the provided private key and certificate and start listening. Otherwise, an http server is created and we start listening. app is the main server where database connection is established and routes are loaded.

server.js

server.js is in the main server:

JavaScript
import express from 'express'
import cors from 'cors'
import mongoose from 'mongoose'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import strings from './config/app.config.js'
import userRoutes from './routes/userRoutes.js'
import carRoutes from './routes/carRoutes.js'
import notificationRoutes from './routes/notificationRoutes.js'
import companyRoutes from './routes/companyRoutes.js'
import locationRoutes from './routes/locationRoutes.js'
import bookingRoutes from './routes/bookingRoutes.js'

const DB_HOST = process.env.BC_DB_HOST
const DB_PORT = process.env.BC_DB_PORT
const DB_SSL = process.env.BC_DB_SSL.toLowerCase() === 'true'
const DB_SSL_KEY = process.env.BC_DB_SSL_KEY
const DB_SSL_CERT = process.env.BC_DB_SSL_CERT
const DB_SSL_CA = process.env.BC_DB_SSL_CA
const DB_DEBUG = process.env.BC_DB_DEBUG.toLowerCase() === 'true'
const DB_AUTH_SOURCE = process.env.BC_DB_AUTH_SOURCE
const DB_USERNAME = process.env.BC_DB_USERNAME
const DB_PASSWORD = process.env.BC_DB_PASSWORD
const DB_APP_NAME = process.env.BC_DB_APP_NAME
const DB_NAME = process.env.BC_DB_NAME
const DB_URI = `mongodb://${encodeURIComponent(DB_USERNAME)}:
${encodeURIComponent(DB_PASSWORD)}@${DB_HOST}:${DB_PORT}/${DB_NAME}?
authSource=${DB_AUTH_SOURCE}&appName=${DB_APP_NAME}`

let options = {}
if (DB_SSL) {
    options = {
        ssl: true,
        sslValidate: true,
        sslKey: DB_SSL_KEY,
        sslCert: DB_SSL_CERT,
        sslCA: [DB_SSL_CA]
    }
}

mongoose.set('debug', DB_DEBUG)
mongoose.Promise = global.Promise
mongoose.connect(DB_URI, options)
    .then(
        () => { console.log('Database is connected') },
        err => { console.error('Cannot connect to the database:', err) }
    )

const app = express()
app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())
app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))
app.use(cors())
app.use('/', userRoutes)
app.use('/', companyRoutes)
app.use('/', locationRoutes)
app.use('/', carRoutes)
app.use('/', bookingRoutes)
app.use('/', notificationRoutes)

strings.setLanguage(process.env.BC_DEFAULT_LANGUAGE)

export default app;

First of all, we build MongoDB connection string, then we establish a connection with BookCars MongoDB database. Then we create an Express app and load middlewares. Finally, we load Express routes and export app.

Routes

There are six routes in BookCars API. Each route has its own controller following the MVC design pattern and SOLID principles. Below are the main routes:

  • userRoutes: Provides REST functions related to users
  • companyRoutes: Provides REST functions related to suppliers
  • locationRoutes: Provides REST functions related to locations
  • carRoutes: Provides REST functions related to cars
  • bookingRoutes: Provides REST functions related to bookings
  • notificationRoutes: Provides REST functions related to notifications

We are not going to explain each route one by one. We'll take, for example, locationRoutes and see how it was made:

JavaScript
import express from 'express'
import routeNames from '../config/locationRoutes.config.js'
import authJwt from '../middlewares/authJwt.js'
import * as locationController from '../controllers/locationController.js'

const routes = express.Router()

routes.route(routeNames.validate).post
            (authJwt.verifyToken, locationController.validate)
routes.route(routeNames.create).post(authJwt.verifyToken, locationController.create)
routes.route(routeNames.update).put(authJwt.verifyToken, locationController.update)
routes.route(routeNames.delete).delete
            (authJwt.verifyToken, locationController.deleteLocation)
routes.route(routeNames.getLocation).get(locationController.getLocation)
routes.route(routeNames.getLocations).get(locationController.getLocations)
routes.route(routeNames.checkLocation).get
            (authJwt.verifyToken, locationController.checkLocation)

export default routes;

First of all, we create an Express Router. Then, we create routes using its name, its method, middlewares and its controller.

routeNames contains locationRoutes route names:

JavaScript
export default {
    validate: '/api/validate-location',
    create: '/api/create-location',
    update: '/api/update-location/:id',
    delete: '/api/delete-location/:id',
    getLocation: '/api/location/:id/:language',
    getLocations: '/api/locations/:page/:size/:language',
    checkLocation: '/api/check-location/:id',
}

locationController contains the main business logic regarding locations. We are not going to see all the source code of the controller since it's quite large but we'll take create and getLocations controller functions for example.

Below is Location model:

JavaScript
import mongoose from 'mongoose'

const Schema = mongoose.Schema

const locationSchema = new Schema({
    values: {
        type: [Schema.Types.ObjectId],
        ref: 'LocationValue',
        validate: (value) => Array.isArray(value) && value.length > 1
    }
}, {
    timestamps: true,
    strict: true,
    collection: 'Location'
})

const locationModel = mongoose.model('Location', locationSchema)

locationModel.on('index', (err) => {
    if (err) {
        console.error('Location index error: %s', err)
    } else {
        console.info('Location indexing complete')
    }
})

export default locationModel;

A Location has multiple values. One value per language. By default, English and French languages are supported.

Below is LocationValue model:

JavaScript
import mongoose from 'mongoose'

const Schema = mongoose.Schema

const locationValueSchema = new Schema({
    language: {
        type: String,
        required: [true, "can't be blank"],
        index: true,
        trim: true,
        lowercase: true,
        minLength: 2,
        maxLength: 2,
    },
    value: {
        type: String,
        required: [true, "can't be blank"],
        index: true,
        trim: true
    }
}, {
    timestamps: true,
    strict: true,
    collection: 'LocationValue'
})

const locationValueModel = mongoose.model('LocationValue', locationValueSchema)

locationValueModel.on('index', (err) => {
    if (err) {
        console.error('LocationValue index error: %s', err)
    } else {
        console.info('LocationValue indexing complete')
    }
})

export default locationValueModel;

A LocationValue has a language code (ISO 639-1) and a string value.

Below is create controller function:

JavaScript
export const create = async (req, res) => {
    const names = req.body

    try {
        const values = []
        for (let i = 0; i < names.length; i++) {
            const name = names[i]
            const locationValue = new LocationValue({
                language: name.language,
                value: name.name
            })
            await locationValue.save()
            values.push(locationValue._id)
        }

        const location = new Location({ values })
        await location.save()
        return res.sendStatus(200)
    } catch (err) {
        console.error(`[location.create]  ${strings.DB_ERROR} ${req.body}`, err)
        res.status(400).send(strings.DB_ERROR + err)
    }
};

In this function, we retrieve the body of the request, we iterate through the values provided in the body (one value per language) and we create a LocationValue. Finally, we create the location depending on the created location values.

Below is getLocations controller function:

JavaScript
export const getLocations = async (req, res) => {
    try {
        const page = parseInt(req.params.page)
        const size = parseInt(req.params.size)
        const language = req.params.language
        const keyword = escapeStringRegexp(req.query.s || '')
        const options = 'i'

        const locations = await Location.aggregate([
            {
                $lookup: {
                    from: 'LocationValue',
                    let: { values: '$values' },
                    pipeline: [
                        {
                            $match: {
                                $and: [
                                    { $expr: { $in: ['$_id', '$$values'] } },
                                    { $expr: { $eq: ['$language', language] } },
                                    { $expr: { $regexMatch: { input: '$value', 
                                               regex: keyword, options } } }
                                ]
                            }
                        }
                    ],
                    as: 'value'
                }
            },
            { $unwind: { path: '$value', preserveNullAndEmptyArrays: false } },
            { $addFields: { name: '$value.value' } },
            {
                $facet: {
                    resultData: [
                        { $sort: { name: 1 } },
                        { $skip: ((page - 1) * size) },
                        { $limit: size },
                    ],
                    pageInfo: [
                        {
                            $count: 'totalRecords'
                        }
                    ]
                }
            }
        ], { collation: { locale: Env.DEFAULT_LANGUAGE, strength: 2 } })

        res.json(locations)
    } catch (err) {
        console.error(`[location.getLocations]  
                        ${strings.DB_ERROR} ${req.query.s}`, err)
        res.status(400).send(strings.DB_ERROR + err)
    }
};

In this controller function, we retrieve locations from database using aggregate MongoDB function and facet to implement pagination.

Below is another simple route, notificationRoutes:

JavaScript
import express from 'express'
import routeNames from '../config/notificationRoutes.config.js'
import authJwt from '../middlewares/authJwt.js'
import * as notificationController from '../controllers/notificationController.js'

const routes = express.Router()

routes.route(routeNames.notificationCounter).get
            (authJwt.verifyToken, notificationController.notificationCounter)
routes.route(routeNames.notify).post
            (authJwt.verifyToken, notificationController.notify)
routes.route(routeNames.getNotifications).get
            (authJwt.verifyToken, notificationController.getNotifications)
routes.route(routeNames.markAsRead).post
            (authJwt.verifyToken, notificationController.markAsRead)
routes.route(routeNames.markAsUnRead).post
            (authJwt.verifyToken, notificationController.markAsUnRead)
routes.route(routeNames.delete).post
            (authJwt.verifyToken, notificationController.deleteNotifications)

export default routes;

Below is the Notification model:

JavaScript
import mongoose from 'mongoose'

const Schema = mongoose.Schema

const notificationSchema = new Schema({
    user: {
        type: Schema.Types.ObjectId,
        required: [true, "can't be blank"],
        ref: 'User',
        index: true
    },
    message: {
        type: String,
        required: [true, "can't be blank"]
    },
    booking: {
        type: Schema.Types.ObjectId,
        ref: 'Booking'
    },
    isRead: {
        type: Boolean,
        default: false
    }
}, {
    timestamps: true,
    strict: true,
    collection: 'Notification'
})

const notificationModel = mongoose.model('Notification', notificationSchema)

notificationModel.on('index', (err) => {
    if (err) {
        console.error('Notification index error: %s', err)
    } else {
        console.info('Notification indexing complete')
    }
})

export default notificationModel;

A Notification is composed of a reference to a user, a message, a reference to a booking and isRead flag.

Below is getNotifications controller function:

JavaScript
export const getNotifications = async (req, res) => {
    try {
        const userId = mongoose.Types.ObjectId(req.params.userId)
        const page = parseInt(req.params.page)
        const size = parseInt(req.params.size)

        const notifications = await Notification.aggregate([
            { $match: { user: userId } },
            {
                $facet: {
                    resultData: [
                        { $sort: { createdAt: -1 } },
                        { $skip: ((page - 1) * size) },
                        { $limit: size },
                    ],
                    pageInfo: [
                        {
                            $count: 'totalRecords'
                        }
                    ]
                }
            }
        ])

        res.json(notifications)
    } catch (err) {
        console.error(strings.DB_ERROR, err)
        res.status(400).send(strings.DB_ERROR + err)
    }
};

In this simple controller function, we retrieve notifications using MongoDB aggregate function, page and size parameters.

Below is markAsRead controller function:

JavaScript
export const markAsRead = async (req, res) => {

    try {
        const { ids: _ids } = req.body, 
        ids = _ids.map(id => mongoose.Types.ObjectId(id))
        const { userId: _userId } = req.params, 
        userId = mongoose.Types.ObjectId(_userId)

        const bulk = Notification.collection.initializeOrderedBulkOp()
        const notifications = await Notification.find({ _id: { $in: ids } })

        bulk.find({ _id: { $in: ids }, 
        isRead: false }).update({ $set: { isRead: true } })
        bulk.execute(async (err, response) => {
            if (err) {
                console.error(`[notification.markAsRead] ${strings.DB_ERROR}`, err)
                return res.status(400).send(strings.DB_ERROR + err)
            }

            const counter = await NotificationCounter.findOne({ user: userId })
            counter.count -= notifications.filter
                             (notification => !notification.isRead).length
            await counter.save()

            return res.sendStatus(200)
        })

    } catch (err) {
        console.error(`[notification.markAsRead] ${strings.DB_ERROR}`, err)
        return res.status(400).send(strings.DB_ERROR + err)
    }
};

In this controller function, we bulk update notifications and mark them as read.

Frontend

The frontend is a web application built with Node.js, React and MUI. From the frontend, the user can search for available cars depending on pickup and drop-off locations, date and time booking. He can then select his booking options and finally checkout.

Image 35

  • ./frontend/assets/ folder contains CSS and images.
  • ./frontend/pages/ folder contains React pages.
  • ./frontend/components/ folder contains React components.
  • ./frontend/services/ contains BookCars API client services.
  • ./frontend/App.js is the main React App that contains routes.
  • ./frontend/index.js is the main entry point of the frontend.

App.js is the main react App:

JavaScript
import React, { lazy, Suspense } from 'react'
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'

const SignIn = lazy(() => import("./components/SignIn"))
const SignUp = lazy(() => import("./components/SignUp"))
const Activate = lazy(() => import('./components/Activate'))
const ForgotPassword = lazy(() => import('./components/ForgotPassword'))
const ResetPassword = lazy(() => import('./components/ResetPassword'))
const Home = lazy(() => import("./components/Home"))
const Cars = lazy(() => import("./components/Cars"))
const CreateBooking = lazy(() => import("./components/CreateBooking"))
const Bookings = lazy(() => import("./components/Bookings"))
const Booking = lazy(() => import("./components/Booking"))
const Settings = lazy(() => import("./components/Settings"))
const Notifications = lazy(() => import("./components/Notifications"))
const ToS = lazy(() => import("./components/ToS"))
const About = lazy(() => import("./components/About"))
const ChangePassword = lazy(() => import("./components/ChangePassword"))
const Contact = lazy(() => import("./components/Contact"))
const NoMatch = lazy(() => import("./components/NoMatch"))

const App = () => {
    return (
        <Router>
            <div className="App">
                <Suspense fallback={<></>}>
                    <Routes>
                        <Route exact path="/sign-in" element={<SignIn />} />
                        <Route exact path="/sign-up" element={<SignUp />} />
                        <Route exact path='/activate' element={<Activate />} />
                        <Route exact path='/forgot-password' element={<ForgotPassword />} />
                        <Route exact path='/reset-password' element={<ResetPassword />} />
                        <Route exact path="/" element={<Home />} />
                        <Route exact path="/cars" element={<Cars />} />
                        <Route exact path="/create-booking" element={<CreateBooking />} />
                        <Route exact path="/bookings" element={<Bookings />} />
                        <Route exact path="/booking" element={<Booking />} />
                        <Route exact path="/settings" element={<Settings />} />
                        <Route exact path="/notifications" element={<Notifications />} />
                        <Route exact path="/change-password" element={<ChangePassword />} />
                        <Route exact path="/about" element={<About />} />
                        <Route exact path="/tos" element={<ToS />} />
                        <Route exact path="/contact" element={<Contact />} />

                        <Route path="*" element={<NoMatch />} />
                    </Routes>
                </Suspense>
            </div>
        </Router>
    )
}

export default App

We are using React lazy loading to load each route.

We are not going to cover each page of the frontend, but you can open the source code and see each one if you want to.

Mobile App

BookCars provides a native mobile app for Android and iOS. The mobile app is built with React Native and Expo. Like for the frontend, the mobile app allows the user to search for available cars depending on pickup and drop-off locations, dates and time booking. He can then select his booking options and finally checkout.

The user receives push notifications when his booking is updated. Push notifications are built with Node.js, Expo Server SDK and Firebase.

Image 36

  • ./mobile/assets/ folder contains images.
  • ./mobile/screens/ folder contains main pages.
  • ./mobile/components/ folder contains React Native components.
  • ./mobile/services/ contains BookCars API client services.
  • ./mobile/App.js is the main React Native App.
  • ./mobile/index.js is the main entry point of the mobile app.

index.js is the main entry point of the mobile app:

JavaScript
import { registerRootComponent } from 'expo'

import App from './App'

// registerRootComponent calls AppRegistry.registerComponent('main', () => App)
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

App.js is the main React Native app:

JavaScript
import 'react-native-gesture-handler'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { RootSiblingParent } from 'react-native-root-siblings'
import { NavigationContainer } from '@react-navigation/native'
import { StatusBar as ExpoStatusBar } from 'expo-status-bar'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import DrawerNavigator from './components/DrawerNavigator'
import { Provider } from 'react-native-paper'
import * as SplashScreen from 'expo-splash-screen'
import * as Notifications from 'expo-notifications'
import Helper from './common/Helper'
import NotificationService from './services/NotificationService'
import UserService from './services/UserService'

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: true,
  }),
})

export default function App() {
  const [appIsReady, setAppIsReady] = useState(false)
  const responseListener = useRef()
  const navigationRef = useRef()

  useEffect(() => {
    async function register() {
      const loggedIn = await UserService.loggedIn()
      if (loggedIn) {
        const currentUser = await UserService.getCurrentUser()
        await Helper.registerPushToken(currentUser.id)
      }
    }

    // Register push notifications token
    register()

    // This listener is fired whenever a user taps on or interacts with a 
    // notification (works when app is foregrounded, backgrounded, or killed)
    responseListener.current = 
    Notifications.addNotificationResponseReceivedListener(async response => {
      try {
        if (navigationRef.current) {
          const data = response.notification.request.content.data

          if (data.booking) {
            if (data.user && data.notification) {
              await NotificationService.markAsRead(data.user, [data.notification])
            }
            navigationRef.current.navigate('Booking', { id: data.booking })
          } else {
            navigationRef.current.navigate('Notifications')
          }
        }
      } catch (err) {
        Helper.error(err, false)
      }
    })

    return () => {
      Notifications.removeNotificationSubscription(responseListener.current)
    }
  }, [])

  useEffect(() => {
    async function prepare() {
      try {
        // Keep the splash screen visible while we fetch resources
        await SplashScreen.preventAutoHideAsync()
        await new Promise(resolve => setTimeout(resolve, 500))
      } catch (err) {
        Helper.error(err)
      } finally {
        // Tell the application to render
        setAppIsReady(true)
      }
    }

    prepare()
  }, [])

  const onReady = useCallback(async () => {
    if (appIsReady) {
      // This tells the splash screen to hide immediately! If we call this after
      // `setAppIsReady`, then we may see a blank screen while the app is
      // loading its initial state and rendering its first pixels. So instead,
      // we hide the splash screen once we know the root view has already
      // performed layout.
      await SplashScreen.hideAsync()
    }
  }, [appIsReady])

  if (!appIsReady) {
    return null
  }

  return (
    <SafeAreaProvider>
      <Provider>
        <RootSiblingParent>
          <NavigationContainer ref={navigationRef} onReady={onReady}>
            <ExpoStatusBar style='light' backgroundColor='rgba(0, 0, 0, .9)' />
            <DrawerNavigator />
          </NavigationContainer>
        </RootSiblingParent>
      </Provider>
    </SafeAreaProvider>
  )
}

We are not going to cover each screen of the mobile app, but you can open the source code and see each one if you want to.

Backend

The backend is a web application built with Node.js, React and MUI. From the backend, the admin user can create and manage suppliers, cars, locations, users and bookings. When the admin user creates a new supplier, the supplier will receive an automatic email for creating his account to access the backend so he can manage his car fleet and bookings.

Image 37

  • ./backend/assets/ folder contains CSS and images.
  • ./backend/pages/ folder contains React pages.
  • ./backend/components/ folder contains React components.
  • ./backend/services/ contains BookCars API client services.
  • ./backend/App.js is the main React App that contains routes.
  • ./backend/index.js is the main entry point of the backend.

App.js of the backend follows similar logic like App.js of the frontend.

We are not going to cover each page of the backend but you can open the source code and see each one if you want to.

Installation

Below are the installation instructions on Ubuntu Linux.

Prerequisites

  1. Install git, Node.js, NGINX, MongoDB and mongosh.

  2. Configure MongoDB:

mongosh

Create admin user:

db = db.getSiblingDB('admin')
db.createUser({ user: "admin" , pwd: "PASSWORD", 
roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

Replace PASSWORD with a strong password.

Secure MongoDB:

sudo nano /etc/mongod.conf

Change configuration as follows:

net:
  port: 27017
  bindIp: 0.0.0.0

security:
  authorization: enabled

Restart MongoDB service:

sudo systemctl restart mongod.service
sudo systemctl status mongod.service

Instructions

  1. Clone BookCars repo:
    cd /opt
    sudo git clone https://github.com/aelassas/bookcars.git
  2. Add permissions:
    sudo chown -R $USER:$USER /opt/bookcars
    sudo chmod -R +x /opt/bookcars/__scripts
  3. Create deployment shortcut:
    sudo ln -s /opt/bookcars/__scripts/bc-deploy.sh /usr/local/bin/bc-deploy
  4. Create BookCars service:
    sudo cp /opt/bookcars/__services/bookcars.service /etc/systemd/system
    sudo systemctl enable bookcars.service
  5. Add /opt/bookcars/api/.env file:
    NODE_ENV = production
    BC_PORT = 4002
    BC_HTTPS = false
    BC_PRIVATE_KEY = /etc/ssl/bookcars.com.key
    BC_CERTIFICATE = /etc/ssl/bookcars.com.crt
    BC_DB_HOST = 127.0.0.1
    BC_DB_PORT = 27017
    BC_DB_SSL = false
    BC_DB_SSL_KEY = /etc/ssl/bookcars.com.key
    BC_DB_SSL_CERT = /etc/ssl/bookcars.com.crt
    BC_DB_SSL_CA = /etc/ssl/bookcars.com.ca.pem
    BC_DB_DEBUG = false
    BC_DB_APP_NAME = bookcars
    BC_DB_AUTH_SOURCE = admin
    BC_DB_USERNAME = admin
    BC_DB_PASSWORD = PASSWORD
    BC_DB_NAME = bookcars
    BC_JWT_SECRET = SECRET
    BC_JWT_EXPIRE_AT = 86400
    BC_TOKEN_EXPIRE_AT = 86400
    BC_SMTP_HOST = in-v3.mailjet.com
    BC_SMTP_PORT = 587
    BC_SMTP_USER = USER
    BC_SMTP_PASS = PASSWORD
    BC_SMTP_FROM = admin@bookcars.com
    BC_ADMIN_EMAIL = admin@bookcars.com
    BC_CDN_USERS = /var/www/cdn/bookcars/users
    BC_CDN_TEMP_USERS = /var/www/cdn/bookcars/temp/users
    BC_CDN_CARS = /var/www/cdn/bookcars/cars
    BC_CDN_TEMP_CARS = /var/www/cdn/temp/bookcars/cars
    BC_DEFAULT_LANGUAGE = en
    BC_BACKEND_HOST = https://bookcars.com:81/
    BC_FRONTEND_HOST = https://bookcars.com/
    BC_MINIMUM_AGE = 21
    BC_EXPO_ACCESS_TOKEN = TOKEN

    You must configure the following options:

    BC_SMTP_HOST
    BC_DB_PASSWORD
    BC_SMTP_USER
    BC_SMTP_PASS
    BC_SMTP_FROM
    BC_ADMIN_EMAIL
    BC_BACKEND_HOST
    BC_FRONTEND_HOST
    BC_EXPO_ACCESS_TOKEN

    If you want to run the mobile app, you'll need to set up BC_EXPO_ACCESS_TOKEN by following these instructions.

    If you want to enable SSL, you must configure the following options:

    BC_HTTPS = true
    BC_PRIVATE_KEY
    BC_CERTIFICATE
  6. Add /opt/bookcars/backend/.env file:
    PORT = 3000
    REACT_APP_NODE_ENV = production
    REACT_APP_BC_API_HOST = https://bookcars.com:4002
    REACT_APP_BC_DEFAULT_LANGUAGE = en
    REACT_APP_BC_PAGE_SIZE = 30
    REACT_APP_BC_CARS_PAGE_SIZE = 15
    REACT_APP_BC_BOOKINGS_PAGE_SIZE = 20
    REACT_APP_BC_CDN_USERS = https://bookcars.com/cdn/bookcars/users
    REACT_APP_BC_CDN_TEMP_USERS = https://bookcars.com/cdn/bookcars/temp/users
    REACT_APP_BC_CDN_CARS = https://bookcars.com/cdn/bookcars/cars
    REACT_APP_BC_CDN_TEMP_CARS = https://bookcars.com/cdn/bookcars/temp/cars
    REACT_APP_BC_COMAPANY_IMAGE_WIDTH = 60
    REACT_APP_BC_COMAPANY_IMAGE_HEIGHT = 30
    REACT_APP_BC_CAR_IMAGE_WIDTH = 300
    REACT_APP_BC_CAR_IMAGE_HEIGHT = 200
    REACT_APP_BC_APP_TYPE = backend
    REACT_APP_BC_MINIMUM_AGE = 21

    You must configure the following options:

    REACT_APP_BC_API_HOST
    REACT_APP_BC_CDN_USERS
    REACT_APP_BC_CDN_TEMP_USERS
    REACT_APP_BC_CDN_CARS
    REACT_APP_BC_CDN_TEMP_CARS
  7. Add /opt/bookcars/frontend/.env file:
    PORT = 3001
    REACT_APP_NODE_ENV = production
    REACT_APP_BC_API_HOST = https://bookcars.com:4002
    REACT_APP_BC_RECAPTCHA_SITE_KEY = GOOGLE_RECAPTCHA_SITE_KEY
    REACT_APP_BC_DEFAULT_LANGUAGE = en
    REACT_APP_BC_PAGE_SIZE = 30
    REACT_APP_BC_CARS_PAGE_SIZE = 15
    REACT_APP_BC_BOOKINGS_PAGE_SIZE = 20
    REACT_APP_BC_CDN_USERS = https://bookcars.com/cdn/bookcars/users
    REACT_APP_BC_CDN_CARS = https://bookcars.com/cdn/bookcars/cars
    REACT_APP_BC_COMAPANY_IMAGE_WIDTH = 60
    REACT_APP_BC_COMAPANY_IMAGE_HEIGHT = 30
    REACT_APP_BC_CAR_IMAGE_WIDTH = 300
    REACT_APP_BC_CAR_IMAGE_HEIGHT = 200
    REACT_APP_BC_APP_TYPE = frontend
    REACT_APP_BC_MINIMUM_AGE = 21

    You must configure the following options:

    REACT_APP_BC_API_HOST
    REACT_APP_BC_RECAPTCHA_SITE_KEY
    REACT_APP_BC_CDN_USERS
    REACT_APP_BC_CDN_CARS
  8. If you want to use the mobile app, you must add mobile/.env:
    BC_API_HOST = https://bookcars.com:4002
    BC_DEFAULT_LANGUAGE = en
    BC_PAGE_SIZE = 20
    BC_CARS_PAGE_SIZE = 8
    BC_BOOKINGS_PAGE_SIZE = 8
    BC_CDN_USERS = https://bookcars.com/cdn/bookcars/users
    BC_CDN_CARS = https://bookcars.com/cdn/bookcars/cars
    BC_COMAPANY_IMAGE_WIDTH = 60
    BC_COMAPANY_IMAGE_HEIGHT = 30
    BC_CAR_IMAGE_WIDTH = 300
    BC_CAR_IMAGE_HEIGHT = 200
    BC_APP_TYPE = frontend
    BC_MINIMUM_AGE = 21

    You must configure the following options:

    BC_API_HOST
    BC_CDN_USERS
    BC_CDN_CARS
  9. Configure NGINX:
    sudo nano /etc/nginx/sites-available/default

    Change the configuration as follows for the frontend:

    server {
        root /var/www/bookcars/frontend;
        #listen 443 http2 ssl default_server;
        listen 80 default_server;
        server_name _;
        
        #ssl_certificate_key /etc/ssl/bookcars.com.key;
        #ssl_certificate /etc/ssl/bookcars.com.pem;
    
        access_log /var/log/nginx/bookcars.frontend.access.log;
        error_log /var/log/nginx/bookcars.frontend.error.log;
    
        index index.html;
    
        location / {
          try_files $uri /index.html =404;
        }
    
        location /cdn {
          alias /var/www/cdn;
        }
    
        location ~ .(static)/(js|css|media)/(.+)$ {
          try_files $uri $uri/ /$1/$2/$3;
        }
    }

    If you want to enable SSL, uncomment these lines:

    #listen 443 http2 ssl default_server
    #ssl_certificate_key /etc/ssl/bookcars.com.key
    #ssl_certificate /etc/ssl/bookcars.com.pem;

    Change the configuration as follows for the backend:

    server {
        root /var/www/bookcars/backend;
        #listen 3000 http2 ssl default_server;
        listen 3000 default_server;
        server_name _;
    
        #ssl_certificate_key /etc/ssl/bookcars.com.key;
        #ssl_certificate /etc/ssl/bookcars.com.pem;
    
        #error_page 497 301 =307 https://$host:$server_port$request_uri;
    
        access_log /var/log/nginx/bookcars.backend.access.log;
        error_log /var/log/nginx/bookcars.backend.error.log;
    
        index index.html;
    
        location / {
          try_files $uri /index.html =404;  
        }
    
        location ~ .(static)/(js|css|media)/(.+)$ {
          try_files $uri $uri/ /$1/$2/$3;
        }
    }

    If you want to enable SSL, uncomment these lines:

    #listen 3000 http2 ssl default_server
    #ssl_certificate_key /etc/ssl/bookcars.com.key
    #ssl_certificate /etc/ssl/bookcars.com.pem
    #error_page 497 301 =307 https://$host:$server_port$request_uri;

    Then, check nginx configuration and start nginx service:

    sudo nginx -t
    sudo systemctl restart nginx.service
    sudo systemctl status nginx.service
  10. enable firewall and open BookCars ports:
    sudo ufw enable
    sudo ufw allow 4002/tcp
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw allow 3000/tcp
  11. Start bookcars service:
    cd /opt/bookcars/api
    npm ci
    sudo systemctl start bookcars.service

    Make sure that bookcars service is running with the following command:

    sudo systemctl status bookcars.service

    Make sure that the database connection is established by checking the logs:

    tail -f /var/log/bookcars.log
  12. Deploy BookCars:
    bc-deploy all

    BookCars backend is accessible on port 3000 and the frontend is accessible on port 80 or 443 if SSL is enabled.

  13. If you don't want to use the demo database, create an admin user by navigating to hostname:3000/sign-up

  14. Open backend/src/App.js and comment this line to secure the backend:

    JavaScript
    <Route exact path='/sign-up' element={<Signup />} />

    And run backend deployment again:

    bc-deploy all

Change Currency

To change currency, follow the instructions below:

Frontend

  1. Open frontend/src/lang/common.js and change en.CURRENCY and fr.CURRENCY.
  2. Open frontend/src/lang/cars.js and change en.CAR_CURRENCY and fr.CAR_CURRENCY.

Backend

Do the same thing for the backend:

  1. Open backend/src/lang/common.js and change en.CURRENCY and fr.CURRENCY.
  2. Open backend/src/lang/cars.js and change en.CAR_CURRENCY and fr.CAR_CURRENCY.

Mobile App

Open mobile/lang/i18n.js and change en.CURRENCY, en.CAR_CURRENCY, fr.CURRENCY and fr.CAR_CURRENCY.

Demo Database

Download bookcars-db.zip down to your machine.

Restore BookCars demo db by using the following command:

mongorestore --verbose --drop --gzip --host=127.0.0.1 
--port=27017 --username=admin --password=$PASSWORD 
--authenticationDatabase=admin --nsInclude="bookcars.*" --archive=bookcars.gz

Don't forget to set $PASSWORD.

Unzip cdn.zip on your web server so that the files will be accessible through http://localhost/cdn/bookcars/.

cdn/bookcars/ contains the following folders:

  • cdn/bookcars/users: This folder contains users avatars and suppliers images.
  • cdn/bookcars/cars: This folder contains cars images.
  • cdn/bookcars/temp: This folder contains temporary files.

Admin user: admin@bookcars.ma
Password: B00kC4r5
Other users Password: B00kC4r5

Run from Code

Below are the instructions to run BookCars from code.

Prerequisites

Install git, Node.js, nginx on Linux or IIS on Windows, MongoDB and mongosh.

Configure MongoDB:

mongosh

Create admin user:

JavaScript
db = db.getSiblingDB('admin')
db.createUser({ user: "admin" , pwd: "PASSWORD", 
roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})

Replace PASSWORD with a strong password.

Secure MongoDB by changing mongod.conf as follows:

net:
  port: 27017
  bindIp: 0.0.0.0

security:
  authorization: enabled

Restart MongoDB service.

Instructions

  1. Download BookCars source code down to your machine.
  2. Add api/.env file:
    NODE_ENV = development
    BC_PORT = 4002
    BC_HTTPS = false
    BC_PRIVATE_KEY = /etc/ssl/bookcars.com.key
    BC_CERTIFICATE = /etc/ssl/bookcars.com.crt
    BC_DB_HOST = 127.0.0.1
    BC_DB_PORT = 27017
    BC_DB_SSL = false
    BC_DB_SSL_KEY = /etc/ssl/bookcars.com.key
    BC_DB_SSL_CERT = /etc/ssl/bookcars.com.crt
    BC_DB_SSL_CA = /etc/ssl/bookcars.com.ca.pem
    BC_DB_DEBUG = false
    BC_DB_APP_NAME = bookcars
    BC_DB_AUTH_SOURCE = admin
    BC_DB_USERNAME = admin
    BC_DB_PASSWORD = PASSWORD
    BC_DB_NAME = bookcars
    BC_JWT_SECRET = SECRET
    BC_JWT_EXPIRE_AT = 86400
    BC_TOKEN_EXPIRE_AT = 86400
    BC_SMTP_HOST = in-v3.mailjet.com
    BC_SMTP_PORT = 587
    BC_SMTP_USER = USER
    BC_SMTP_PASS = PASSWORD
    BC_SMTP_FROM = admin@bookcars.com
    BC_ADMIN_EMAIL = admin@bookcars.com
    BC_CDN_USERS = /var/www/cdn/bookcars/users
    BC_CDN_TEMP_USERS = /var/www/cdn/bookcars/temp/users
    BC_CDN_CARS = /var/www/cdn/bookcars/cars
    BC_CDN_TEMP_CARS = /var/www/cdn/bookcars/temp/cars
    BC_DEFAULT_LANGUAGE = en
    BC_BACKEND_HOST = http://localhost:3000/
    BC_FRONTEND_HOST = http://localhost:3001/
    BC_MINIMUM_AGE = 21
    BC_EXPO_ACCESS_TOKEN = TOKEN

    You must configure the following options:

    BC_DB_PASSWORD
    BC_SMTP_USER
    BC_SMTP_PASS
    BC_SMTP_FROM
    BC_ADMIN_EMAIL
    BC_BACKEND_HOST
    BC_FRONTEND_HOST
    BC_EXPO_ACCESS_TOKEN

    Install nodemon:

    npm i -g nodemon

    Run api:

    cd ./api
    npm install
    npm run dev
  3. Add backend/.env file:
    PORT = 3000
    REACT_APP_NODE_ENV = development
    REACT_APP_BC_API_HOST = http://localhost:4002 
    REACT_APP_BC_DEFAULT_LANGUAGE = en
    REACT_APP_BC_PAGE_SIZE = 30
    REACT_APP_BC_CARS_PAGE_SIZE = 15
    REACT_APP_BC_BOOKINGS_PAGE_SIZE = 20
    REACT_APP_BC_CDN_USERS = http://localhost/cdn/bookcars/users
    REACT_APP_BC_CDN_TEMP_USERS = http://localhost/cdn/bookcars/temp/users
    REACT_APP_BC_CDN_CARS = http://localhost/cdn/bookcars/cars
    REACT_APP_BC_CDN_TEMP_CARS = http://localhost/cdn/bookcars/temp/cars
    REACT_APP_BC_COMAPANY_IMAGE_WIDTH = 60
    REACT_APP_BC_COMAPANY_IMAGE_HEIGHT = 30
    REACT_APP_BC_CAR_IMAGE_WIDTH = 300
    REACT_APP_BC_CAR_IMAGE_HEIGHT = 200
    REACT_APP_BC_APP_TYPE = backend
    REACT_APP_BC_MINIMUM_AGE = 21

    Run backend:

    cd ./backend
    npm install
    npm start
  4. Add frontend/.env file:
    PORT = 3001
    REACT_APP_NODE_ENV = development
    REACT_APP_BC_API_HOST = http://localhost:4002
    REACT_APP_BC_RECAPTCHA_SITE_KEY = GOOGLE_RECAPTCHA_SITE_KEY
    REACT_APP_BC_DEFAULT_LANGUAGE = en
    REACT_APP_BC_PAGE_SIZE = 30
    REACT_APP_BC_CARS_PAGE_SIZE = 15
    REACT_APP_BC_BOOKINGS_PAGE_SIZE = 20
    REACT_APP_BC_CDN_USERS = http://localhost/cdn/bookcars/users
    REACT_APP_BC_CDN_CARS = http://localhost/cdn/bookcars/cars
    REACT_APP_BC_COMAPANY_IMAGE_WIDTH = 60
    REACT_APP_BC_COMAPANY_IMAGE_HEIGHT = 30
    REACT_APP_BC_CAR_IMAGE_WIDTH = 300
    REACT_APP_BC_CAR_IMAGE_HEIGHT = 200
    REACT_APP_BC_APP_TYPE = frontend
    REACT_APP_BC_MINIMUM_AGE = 21

    You must configure the following option:

    REACT_APP_BC_RECAPTCHA_SITE_KEY

    Run frontend:

    cd ./frontend
    npm install
    npm start
  5. If you want to use the mobile app, you must add mobile/.env:
    BC_API_HOST = http://localhost:4002
    BC_DEFAULT_LANGUAGE = en
    BC_PAGE_SIZE = 20
    BC_CARS_PAGE_SIZE = 8
    BC_BOOKINGS_PAGE_SIZE = 8
    BC_CDN_USERS = http://localhost/cdn/bookcars/users
    BC_CDN_CARS = http://localhost/cdn/bookcars/cars
    BC_COMAPANY_IMAGE_WIDTH = 60
    BC_COMAPANY_IMAGE_HEIGHT = 30
    BC_CAR_IMAGE_WIDTH = 300
    BC_CAR_IMAGE_HEIGHT = 200
    BC_APP_TYPE = frontend
    BC_MINIMUM_AGE = 21

    You must configure the following options:

    BC_API_HOST
    BC_CDN_USERS
    BC_CDN_CARS

    You need to replace localhost with your IP address.

  6. Configure http://localhost/cdn
    • On Windows, install IIS and create C:\inetpub\wwwroot\cdn\bookcars folder.
    • On Linux, install Nginx and add cdn folder by changing /etc/nginx/sites-available/default as follows:
    server {
        listen 80 default_server;
        server_name _;
        
        ...
    
        location /cdn {
          alias /var/www/cdn;
        }
    }
  7. Create an admin user from http://localhost:3000/sign-up
  8. To run the mobile app, simply download Expo app on your device and run the following commands from ./mobile folder:
    npm i -g expo-cli
    npm install
    npm start

You need to download google-services.json file and place it in ./mobile root directory for push notifications. Otherwise, the mobile app won't start. You can find detailed instructions regarding the mobile app in the next section.

Run Mobile App

Download google-services.json file and place it in ./mobile root directory for push notifications. Otherwise, the mobile app won't start. Its path can be configured from ./mobile/app.json configuration file through googleServicesFile setting option.

To run the mobile app, create ./mobile/.env file with the following options:

BC_API_HOST = http://localhost:4002
BC_DEFAULT_LANGUAGE = en
BC_PAGE_SIZE = 20
BC_CARS_PAGE_SIZE = 8
BC_BOOKINGS_PAGE_SIZE = 8
BC_CDN_USERS = http://localhost/cdn/bookcars/users
BC_CDN_CARS = http://localhost/cdn/bookcars/cars
BC_COMAPANY_IMAGE_WIDTH = 60
BC_COMAPANY_IMAGE_HEIGHT = 30
BC_CAR_IMAGE_WIDTH = 300
BC_CAR_IMAGE_HEIGHT = 200
BC_APP_TYPE = frontend
BC_MINIMUM_AGE = 21

You must configure the following options:

BC_API_HOST
BC_CDN_USERS
BC_CDN_CARS

You need to replace localhost with your IP address.

Install demo database by following these instructions.

Configure http://localhost/cdn

  • On Windows, install IIS and create C:\inetpub\wwwroot\cdn\bookcars folder.
  • On Linux, install NGINX and add cdn folder by changing /etc/nginx/sites-available/default as follows:
server {
    listen 80 default_server;
    server_name _;
    
    ...

    location /cdn {
      alias /var/www/cdn;
    }
}

Configure ./api by following these instructions.

Run ./api with the following command:

Shell
cd ./api
npm run dev

Run mobile app by simply downloading Expo app on your device and running the following commands from ./mobile folder:

Shell
cd ./mobile
npm i -g expo-cli
npm install
npm start

Open Expo app on your device and scan the QR code to run BookCars mobile app.

Points of Interest

I started this project for a friend who needed a website and a mobile app for renting cars. I then decided to make the source code available and share my experience with other developers. Creating the mobile app with React Native and Expo is very straightforward. Expo makes mobile development with React Native very easy.

Using the same language (JavaScript) for both backend and frontend development is very nice and simple.

That's it! I hope you enjoyed reading this article.

History

  • 10th November, 2022 - Initial release
  • 19th November, 2022 - Updates in source code and content
  • 26th November, 2022 - Added Run Mobile App instructions
  • 1st December, 2022 - Updates in the backend, the frontend and the mobile app
  • 4th December, 2022 - Updates in the backend, the frontend and the mobile app
  • 9th December, 2022 - Updates in the backend and the frontend
  • 23rd January, 2023 - Updates in installation instructions
  • 31st January, 2023 - Updates in source code and instructions

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Engineer
Morocco Morocco
I am a graduate engineer in software computing (ENSEIRB, France, 2010). I work with Node.js, React, React Native, MongoDB, .NET and C#. I have worked with C, C++, Lisp, OCaml, Swift and Java. I like to work with advanced programming techniques, design patterns and SOLID principles. I craft websites in my spare time.

My open source projects:

- Wexflow: .NET Workflow Engine and Automation Platform
- BookCars: Car Rental Platform with Mobile App
- wexCommerce: eCommerce Platform on Next.js
- Wexstream: Video Conferencing Platform

If you'd like to discuss any sort of opportunity, feel free to contact me through my
GitHub profile.

Comments and Discussions

 
QuestionIOS app error build: unable to open configuration settings file Pin
Member 139938811-Feb-23 6:43
Member 139938811-Feb-23 6:43 
AnswerRe: IOS app error build: unable to open configuration settings file Pin
Akram El Assas1-Feb-23 6:53
Akram El Assas1-Feb-23 6:53 
GeneralAwesome Pin
Santanu Bhattacharya 202213-Dec-22 5:51
Santanu Bhattacharya 202213-Dec-22 5:51 
QuestionWow! Pin
cplas5-Dec-22 9:21
cplas5-Dec-22 9:21 
Question5 stars Pin
Alan Hawky20-Nov-22 11:21
Alan Hawky20-Nov-22 11:21 
GeneralMy vote of 5 Pin
Hemant Trivedi10-Nov-22 21:37
Hemant Trivedi10-Nov-22 21:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.