Hi, welcome to the last post of Online Food Ordering App series.
In the previous posts we built API endpoints for authentication and managing orders. We've also built our frontend react and react-native apps and connected them to the API endpoints.
In this post, we are going to implement the view orders list, view single order, update order status, view menu, and place order features on our front-end apps.
Before we start, take a look at this PR and update the backend code. We added the payments endpoint, a script to create the menu and we updated the fetch orders list to accommodate both the admin and the customer.
Mobile app
Before we begin our implementation, let's think for a minute about the user flow we want for our customer.
A logged-in customer launches the app and they immediately see a list of menu items divided into 3 tabs (Breakfast, Lunch/Dinner, and Drinks). Each item has an image, a name, a short description, a cost/price, and size. To switch to a different tab, a user swipes the screen left or right, or they tap on the tab name. To add an item to the cart, a user simply taps on the item. Tapping on the item already in the cart increases its quantity by 1. To remove an item from the cart a user simply taps on the item from the cart screen. From the cart screen, a user can navigate to the payment screen where they will be able to confirm their order by making payment by card.
A user can also see the list of orders he/she has placed and their details by tapping on the basket icon in the bottom navigator. Finally, a user will be able to see his/her account information by tapping on the account icon in the bottom navigator.
The screens of our app will be divided into 2 main parts (AuthStack
and HomeStack
) whereby AuthStack
will contain all the screens related to authentication (LoginScreen
, SignupScreen
, and VerifyScreen
) and HomeStack
will contain nested stacks (MainStack
, OrdersStack
, and AccountStack
).
MainStack
will contain screens allowing the user to view the menu, interact with the cart, and make a payment.
OrdersStack
as the name suggest will contain screens for viewing the list of orders a user has placed and each order details.
AccountStack
will contain just one screen to display the user's account information and a logout button.
Great! Let's get started.
- Install the dependencies we are going to need:
yarn add react-native-dotenv react-native-credit-card-input react-native-paper-tabs react-native-pager-view @react-navigation/material-bottom-tabs react-native-stripe-payments
- In the context directory, create a
dataReducer.js
file and paste the following code inside:
ADD_MENU
: will receive an array of menu categories and their items and will save it in our state in a variable called menu.
GET_MENU
: will receive a category name then loops through the menu categories to find if that category exists then will save it's items in a variable called menuItems.
ADD_TO_CART
: will receive a menu item and append it to a variable called cart.
UPDATE_CART
: will receive an item then checks if that item is in the cart before replacing it with the new item.
REMOVE_FROM_CART
: will receive an item id then loops through the cart array to find an item with that id and delete it.
CLEAR_CART
: will remove all items in the cart array.
ADD_ORDERS
: will receive an array of orders list the save it in the state in a variable called ordersList.
In this file we create a MenuTabs
component that receive 2 props: menuItems
(an array of menu items for the selected category) and handleChangeIndex
(a function to switch tabs). We created a handleAddTocart
function which will help us to modify an item before adding it to the cart and to dispatch messages after the item is added to the cart.
The component returns 3 tab screens where each tab screen will use the ListItems component to display the data or the CustomCaption
component to display that items were not found. Also, each tab screen is associated with an index number starting from 0. We'll see how this index number will be useful in a minute.
Let us now create the main screen and use the menu tabs we just created above.
- Create a
src/screens/MainScreen/MainScreen.js
file like this:
In this file we created a MainScreen
component that fetches user data, cart and menu items from our global state. We created a handleChangeIndex
function that receives an index number (tab screen index) and dispatches a function that will trigger the GET_MENU
action. We used the useEffect hook to trigger the handleChangeIndex function when this component mounts to get data for the first tab screen.
This component will render a welcome message, the user's address, the menuTabs component and the CartButton
component to view the cart contents if the cart is not empty.
Let us now create the last screen for MainStack
.
- Create a
src/screens/PaymentScreen/PaymentScreen.js
file like this:
In this file we created a PaymentScreen
component that has 2 functions: handlePaymentInit
and handleCreditCardForm
. When this component mounts, it displays a title, an image of credit/debit cards accepted, and a button to make payment. When the button is clicked, it triggers the handlePaymentInit
function which triggers an internal state update of showCardForm
to display the CreditCardForm
component.
The CreditCardForm
component receives an onChange
props which is a function that gets executed as we fill the form and returns a formData
object composed of 3 properties: valid
, values
, and status
. We are interested in valid
and values
properties.
valid
is a boolean which will be true once all the fields of the form are filled correctly.
values
is an object of the form field values. It has the following properties: number
(card number), expiry
(MM/YY), and cvc
(3-digit cvc/ccv). Learn more here.
So, in the handleCreditCardForm
function we check if the user has filled the form correctly, then we extract the form values and build a cardDetails
object. We then proceed to validate the cardDetails object by using the isCardValid
method from react-native-stripe-payments.
If the cardDetails are valid, we hit our API endpoint for payments to create a paymentIntent
. The payment intent returned from calling our API contains a clientSecret
string that we use along with the cardDetails object to confirm the payment with stripe.
If the response returned from confirming the payment contains an id, that means the payment is successful then we proceed to prepare the payload for the order and hit our backend endpoint to place an order. If the order is placed successfully, we reset our stack navigation then navigate to ordersListScreen.
NOTE: this implementation is a bit naive because there are some edge cases we did not account for, for example, what if the payment is successful but the order cannot be placed? What happens then?
One solution would be to extend our order statuses and allow a user to place an order before making the payment then once the payment is confirmed we approve the order.
Cool!
Lastly, we wrapped everything in a try and catch so if anything goes wrong the user will be notified via the Alert
component.
NOTE: our services in src/utils/api.js
were starting to become messy, so we refactored the code to look like this:
Make sure you update the authentication feature to use the updated services as well.
The screens for our MainStack are now done. Let us implement OrdersListScreen
and OrderDetailsScreen
for OrdersStack
next.
- Create a
src/screens/OrdersListScreen/OrdersListScreen.js
file like this:
In the OrdersListScreen
component we used the useEffect hook to add a focus
event listener which will be triggered every time this screen is focused. In the event listener we fetch the list of orders and dispatch an action to save the data in ordersList
global state variable. The component will display the list of orders if found or a no orders found text. We have also implemented a handleOrder
function which will receive an id then navigates to OrderDetailsScreen
.
- Create a
src/screens/OrderDetailsScreen/OrderDetailsScreen.js
file like this:
In this component we fetch an order's details by using the orderId parameter from props, save the data in internal state variable orderDetails then we render the information.
The screens for OrdersStack
are now done. Let us create the one screen for AccountStack
.
- Create a
src/AccountScreen/AccountScreen.js
file like this:
In this component we are just displaying the user information from the global state variable auth
. We have also moved our logout implementation in this component.
Now that our screens are done, let us create the stacks mentioned above.
Inside
src/navigation
create a new directory calledstacks
and inside stacks create the following files:MainStack.js
,OrdersStack.js
, andAccountStack.js
.
The last piece of the puzzle is to put together the stacks we created above and add the data context provider in App.js
.
Let's do it.
- Move
HomeStack.js
insrc/navigation/stacks/
and update it to look like this:
Let's go over what we did in this file.
HomeStack
is the component that will be mounted as soon as a user logs in or when a logged-in user launches the app and he/she is authenticated. There are a couple of things we want to do before this component renders:
- We need to fetch the menu and save it in our global state.
- While fetching the menu, if the user's token happens to be expired (from the backend) we automatically log out the user (on the frontend).
- If the user's token is valid and the menu data is found or not, we proceed to render the tab navigator.
Finally, update src/App.js
to look like this:
- Run the app on an emulator or a physical device and you should see the screens below:
To create a splash/launch screen, check out this article.
For reference, here's the repo for the project.
Admin view orders list, view single order and update order
For the admin app we are going to use Material UI's Collapsible Table component to display the orders. Each row in the table will have a button to reveal the details where the admin will be able to see the contents of an order along with a Update status
and user info
buttons to update the status of the order and view the details of the user respectively.
We have also implemented pagination to 5 rows per page but you can change this value according to your needs.
Great. Let's start by installing React Spring to add small animations to our app.
- Install React Spring:
$ yarn add react-spring
In this component we fetch the list of orders from the backend then use the CustomTable component to display the data.
we have also used the useSpring
hook from React Spring to add a fade animation to our component.
Among the new components we created include CustomTableRow, CustomTableFooter, and TablePaginationActions and the result will look like the image below:
And that's it, the admin will now be able to view and update orders.
NOTE: There are lots of features we can add to improve our app. For instance, the first page of the dashboard could contain a summary or an overview of the data in our app like the number of orders for a given period of time, the amount of profit made, the most bought items, etc. We could also leverage the power of websockets to make our app show real-time data or add notifications for when an order is placed or any other action.
This concludes our series.
To recap, we've built a REST API using Node, Express, and Postgres then we built frontend apps in React and React-Native to use the API. We've also covered JWT authentication, unit, integration, and end-to-end testing along with continuous integration and continuous delivery (CI/CD).
I hope this series was useful to you. If you have a question, a comment, or a suggestion let me know in the comment box below.
Thank you for your time, until next time, cheers!
Top comments (0)