When you develop the app please be very careful about the infinite loop. When infinite loop happens you may risk reaching the daily read limit of Firebase because you keep re-render some components and those components in turn keep fetching the resource. I cannot find a way to limit the read limit per seconds for Firebase, so please aware of the situation
One way I find to keep track of the infinite loop that pose risks of infinite fetching data is to look at the Network
tab in the Developer Tools (if you are using Chrome you can access with shortcut Ctrl+Shift+I
). If you see a lot of tab popping up nonstop then stop the app immediately or close the browser
Edit: I made some change to the code so it should detect whether you have infinite useEffect
run. Still, good coding practice is never too much
This project use React as front-end framework and rely entirely on client-side rendering. It uses Firebase to handle back-end logic, thus has little to no need to create a dedicated server
This project use React ContextAPI to handle states across components and pages. In consideration of future development and the current team level, I decide to use the Redux approach, meaning components can access app's states and change them by calling certain hooks, and these custom hooks will handle all the logic from changing app state to update Firestore database. This approach, I believe, can reduce the complexity of the app in the long run and make collaboration between developers a bit easier
This project handle pages using react-router
. Nothing special here, and the route structure shall be discussed in later section
For the context structure, I have a file name types.ts
that list out how the overall app states would look like. The very top states are, in no particular order, products
which store the details of all products including the deals and category, alert
which store the notification to display to user, and user
to store user's info as well as cart and order information
products
context is defined inproduct-context.js
filealert
context is defined inalert-context.js
user
context is defined inuser-context.js
These contexts are combined into one in the app-context.js
. If you look closely in app-context.js
, you will see another layer of context, namely AppContext
. This is used to pass the firestore
reference and authentication
around the application. This is the way to access the firestore database in latest version of firebase
In each of those context files there are two parts that may go through changes in the future (change mean we will add some more code, not change the fundamental structure): a reducer (one function with "Reducer" in its name) and several custom hooks. The reducer job is to actually make change to the context. We do so by calling special function called dispatch
and pass inside it the necessary information. The detail on how to use those function, read more on these How to use React Context effectively and How to optimize your context value. One big note here is that these reducer function MUST HAVE NO SIDE-EFFECT
As for the custom hooks, it is used to make the bridge between the context and React component: component use these custom hooks to access and make change to the context. We will do the dispatch
inside these custom hooks and not inside the React component. We also perform our logic like get data from firestore or update database or authenticate user from these hooks. This help decouple the logic from our component and make future development easier
For now, we will have seven(7) basics routes: home, search, category, order, account, auth and checkout. The detail of each route can be found in the dedicated Guide to path
React component shall be defined in files that are in correct directory according to the page they are in. For example, Item.js
that display item in the home page should be in the directory /home
Another note is that, for consistency, we should have component defined inside its own directory of same name (Item.js
is in Item
directory). This is so that if we have CSS file for the component or test file for them, we can put them in same directory
We will have a file of route name that incorporate all those component into one complete design. For example, the route /home
will have a component /home/Home.js
that combine all of components that are needed into one file
For the most part, a component is not shared between route. But at the moment, there are two component that are shared between routes
- Bottom navigation: the bottom navigation used to navigate between routes. No explanation here
- Cart: the hovering "Show cart" right above the bottom navigation should be shared between routes (except for the
/auth
route and probably the/account
). This is, according to the UI design, the only way to access the cart. This component shall do following task: show the current number of items in cart, and when being click on shall pop up a modal that show all the items in cart, the detail quantities and such
The header, although sounds like something that should be shared, isn't actually one as. From the UI design, I see that the header are different between routes and no duplication here
After you cloned the project down to your computer, follow these steps to set up your firebase
-
Create a firebase project and then install the Firebase Stripe Extension and Firebase Trigger mail. Follow all instructions there
-
For the Firebase Stripe Extension, you should choose option "Sync" for the "Sync new users to Stripe customers and Cloud Firestore". After you install the extension, don't forget to setup Stripe Webhook as instructed by the extension (see the section Configure Stripe webhooks or POSTINSTALL instruction). The overall setup look like this
CUSTOMERS_COLLECTION=users DELETE_STRIPE_CUSTOMERS=Auto delete LOCATION=us-west2 PRODUCTS_COLLECTION=products STRIPE_CONFIG_COLLECTION=configuration SYNC_USERS_ON_CREATE=Sync STRIPE_API_KEY=rk_test_51LFbOFBFL4Le4n4LMEoeONWCjmqo73EUhFozs0LoJ4NIPctxx4w004BoFeBrd STRIPE_WEBHOOK_SECRET=whsec_LYb6SrfCBNwjRQU5
-
For Firebase Trigger mail, we have to set up a SMTP connection URI and SMTP password. I am using Sendgrid because it's free and easiest to set up. The overall setup would look like this
[email protected] LOCATION=us-west2 MAIL_COLLECTION=emails SMTP_CONNECTION_URI=smtps://[email protected]:465 SMTP_PASSWORD=SG.v3wmh4HvTf6GKNNi5qsXKA.Nd1LUtQdIqDQcxjG1NuJI
-
Create a local secret named
STRIPE_API_KEY
andOTP_SECRET
by runningfirebase functions:secrets:set STRIPE_API_KEY # you will be prompted to enter the key firebase functions:secrets:set OTP_SECRET # same as above, you will be prompted to enter the key
Make sure the key you parse in here have sufficient permissions to write to products (in the
STRIPE_API_KEY
key). For more information about managing API key, see Store and access sensitive configuration information -
Upload the custom cloud functions by running
firebase deploy --only functions
-
Upload the security rules
firebase deploy --only firestore:rules
- Install extension. This will prompt you to enter information. For now, just save all info in local. You don't have to do
npm run build
because it seems like when you install they automatically compile to normal Javascript file. Make sure you enter the correct information - Go to where the extension is and install all necessary libraries with
npm install
(IMPORTANT). I don't know why they didn't do that automatically. For example, the extension, after runningfirebase emulator:start
is located atC:/User/name/.cache/firebase/extension
- Stop the emulator and run it again. You should have the extension run without error (not mean they will work)
Now for each extension above. Trigger Email should work, but Stripe Payment need some change. Remember we need to have the Webhook setup. Stripe Payment uses HTTPS call to listen to Stripe event (like payment created, checkout created, etc.), mean that we need to have the HTTPS call URL. See Instrument your app for HTTPS functions emulation. Your URL should look something like this http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents
. You can get this URL by looking at the initialized messages as such
firestore: Firestore Emulator logging to firestore-debug.log
i hosting: Serving hosting files from: build
+ hosting: Local server: http://localhost:5000
i ui: Emulator UI logging to ui-debug.log
i functions: Watching "D:\Coding\Job\dormit\second-prototype\functions" for Cloud Functions...
+ functions[us-central1-checkout]: firestore function initialized.
+ functions[us-central1-sendCodeViaEmail]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/sendCodeViaEmail).
+ functions[us-central1-verifyOtpCode]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/verifyOtpCode).
+ functions[us-central1-updateEmail]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/updateEmail).
+ functions[us-central1-updateShipping]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/updateShipping).
+ functions[us-central1-updateUserProfile]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/updateUserProfile).
i functions: Watching "C:\Users\asada\.cache\firebase\extensions\firebase\[email protected]\functions" for Cloud Functions...
+ functions[us-west2-ext-firestore-send-email-processQueue]: firestore function initialized.
i functions: Watching "C:\Users\asada\.cache\firebase\extensions\stripe\[email protected]\functions" for Cloud Functions...
+ functions[us-west2-ext-firestore-stripe-payments-createCustomer]: auth function initialized.
+ functions[us-west2-ext-firestore-stripe-payments-createCheckoutSession]: firestore function initialized.
+ functions[us-west2-ext-firestore-stripe-payments-createPortalLink]: http function initialized (http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-createPortalLink).
+ functions[us-west2-ext-firestore-stripe-payments-handleWebhookEvents]: http function initialized (http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents).
After that, you have to use local Stripe CLI to test out webhook. Because we work on local, we have to find a way for Stripe to connect to our machine. See Is it possible to set localhost as a Stripe webhook URL? and watch the video Laravel Stripe Checkout tutorial on what the process would look like
One more thing. Because this is run on local, you can modify the extension as you want. Just don't forget to recompile them. And don't worry if you go to the extension site (usually at http://localhost:4000/extensions
) and see that some fields in configuration are empty. They are secret fields and usually remained empty like that, though in reality the emulator already has info from your above configuration (either local file or remote Google Safe)
If you did everything correctly, when you perform checkout or sign up, this is what you should see
stripe listen --forward-to http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents
> Ready! You are using Stripe API Version [2020-08-27]. Your webhook signing secret is whsec_05764f2e03350dcede056065965787869f5a5ddffe300e2b6790c3917e9dccae (^C to quit)
2022-07-29 06:48:52 --> customer.created [evt_1LQtYeBFL4Le4n4L6tPdkEye]
2022-07-29 06:48:53 <-- [200] POST http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents [evt_1LQtYeBFL4Le4n4L6tPdkEye]
2022-07-29 06:54:22 --> customer.created [evt_1LQtdyBFL4Le4n4LVboDDxKQ]
2022-07-29 06:54:22 <-- [200] POST http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents [evt_1LQtdyBFL4Le4n4LVboDDxKQ]
2022-07-29 06:54:23 --> payment_intent.created [evt_3LQtdzBFL4Le4n4L07rARS9D]
2022-07-29 06:54:23 <-- [200] POST http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents [evt_3LQtdzBFL4Le4n4L07rARS9D]
2022-07-29 06:54:56 --> customer.updated [evt_1LQteWBFL4Le4n4L2ODl9KwP]
2022-07-29 06:54:56 --> payment_intent.succeeded [evt_3LQtdzBFL4Le4n4L0LDr4sII]
2022-07-29 06:54:56 --> checkout.session.completed [evt_1LQteWBFL4Le4n4LbIuuz5gQ]
2022-07-29 06:54:56 --> charge.succeeded [evt_3LQtdzBFL4Le4n4L0TXpwGva]
2022-07-29 06:54:56 <-- [200] POST http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents [evt_1LQteWBFL4Le4n4L2ODl9KwP]
2022-07-29 06:54:57 <-- [200] POST http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents [evt_3LQtdzBFL4Le4n4L0LDr4sII]
- Create an account and activate it
- Set up the billing so that we can have the checkout page to enter card info
- Create product in the Product page. All products must have quantity in the metadata
- Set up tax
-
How to use the firebase?
Read the Get started with Cloud Firestore and Get Started with Firebase Authentication on Websites. Remember to use the Web version 9
For this app, replace the detail in
firebase.config.js
with the correct setup and you should be good to go -
How do I contribute to the application
You can fork the project and then do pull request. Ideally, you would want to create a new branch, do some change on it and do pull request on those. Talk or message with the leads for more information
-
Should I use this library X to do Y?
Please talk or message with the leads before you decide to install a library. This is front-end, and unlike back-end, we are very sensitive to how much "weight" does the app have. If the application is too heavy, it will make the page sluggish and the user will leave the website. A good rule-of-thumb to decide a library is to think: can I do this without the library and how much work do I have to do; how much weight the library will add into the project; how much the library is used comparing to the size of it
-
Why do you store info of cart in
Context
and inlocalStorage
? Would this make it redundant?This is to store the data of cart when user is not authenticated. Data stored in
Context
will be lost the moment we close the tab or refresh the page. To ensure the app working properly, always make data about cart in theContext
andlocalStorage
in sync with each other -
I cannot deploy the cloud function/security rules
This can happen because you miss some steps along the way. However, there is one strange bug I encountered while working with Firebase: I was denied of deploying because I didn't authenticated despite the fact that I already verified that I did sign in. To fix this, you just sign out (by running
firebase logout
) then sign back in again (firebase login
) -
Why the app use so much read?
There are many reasons for this to happen. Before jumping to conclusion, make sure that this is the actual problem and high read count is consistent. Sometimes, right after some deployment the read count can spike. For example, I encountered some read count spikes right after I deployed cloud function. The read count normalizes after a bit
-
I got some error related to tax
Watch this video How to fix tax_behavior missing for prices error
-
The cloud function doesn't send custom token. What happened?
Probably this one. You want to edit the one with "App Engine default service account"
-
For Emulator, I cannot install the extension. I keep getting error
firebase ref does not have a version extension
Probably because you didn't specify the version of the extension when you install it. Some extension doesn't require the version to be specified but some extension does require. You typically just use the latest version found in the extension page and install like this
firebase ext:install stripe/[email protected]
-
When I run
firebase emulators:start
, I getting error likeThe Cloud Functions emulator requires the module "firebase-admin" to be installed
First thing you should do is to update your
firebase-tools
to the latest version. If this still not works, you may have to install all dependencies that the extension needed by yourselves. Extension is just cloud function and they have dependencies. You can view where the extension is installed when you install the extension or when you runfirebase emulators:start
In Windows, the extension is installed at
~/.cache/firebase/extensions
. For example, my Stripe payment extension is installed atC:\Users\my-username\.cache\firebase\extensions\stripe\[email protected]
. When you go to the extension installed location, navigate to thefunctions
directory inside them (all extensions should have that directory - this is where Cloud functions is located). Once there, you runnpm install
to install all dependencies needed. If the extension use TypeScript, you will need to compile them down to Javascript, but most likely you don't need to because extension will automatically compile when you runnpm install
. You can read theirpackage.json
to see how this works -
I don't see any secret config when I go to
http://localhost:4000/extensions
. I only see config that is not secretEmulator will not display the secret in
.secret.local
and will leave the field blank (yes, blank, not even filled). This is, in my opinion, a very bad UI because it confuses people into thinking that something is wrong and the Emulator didn't get the secret file. In reality, the Emulator did get the secrets -
I cannot setup the Webhooks secret for Stripe
Because you develop in local environment, you cannot set up Webhooks endpoint (how can you put
localhost:5001
as endpoint?). With this, you must use Stripe CLI to "tunnel" the event happen on Stripe to your local machine. See Test a webhooks integration with the Stripe CLI and watch the video Laravel Stripe Checkout tutorial to see how it is in action.Don't quit the CLI because if you do so, it will stop the tunneling. You need to keep it open
In particular, if you are using Stripe payment extension, you probably want to run this for tunneling
# for log in to stripe via CLI stripe login # for tunneling stripe listen --forward-to http://localhost:5001/test-app-8c148/us-west2/ext-firestore-stripe-payments-handleWebhookEvents
-
I keep getting 404 message when I use tunneling
You may have incorrect endpoint. Make sure you check that
-
You use correct protocol. In local development you probably use
http
and nothttps
. They won't mix together -
The function you call is deployed. You can check whether your function is deployed (in emulator) or not by checking the message when you run
firebase emulators:start
. For example, let's say you have function namedmyFunc
and the message you get when start emulator is such# some message above functions[us-central1-checkout]: firestore function initialized. + functions[us-central1-sendCodeViaEmail]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/sendCodeViaEmail). + functions[us-central1-verifyOtpCode]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/verifyOtpCode). + functions[us-central1-updateEmail]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/updateEmail). + functions[us-central1-updateShipping]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/updateShipping). + functions[us-central1-updateUserProfile]: http function initialized (http://localhost:5001/test-app-8c148/us-central1/updateUserProfile). # and some message below
And you don't see your function
myFunc
there. It means something is wrong with yourmyFunc
and it wasn't get deployed to Emulator -
You use correct format for endpoint. By that I mean, when using Emulator, the endpoint will look like
https://localhost:5001/test-app-01b2/us-central1/helloWorld
whereas in production (when you deploy your cloud function to firebase) it will be of formathttps://us-central1-test-app-01b2.cloudfunctions.net/helloWorld
-
-
I cannot config Stripe to show the shipping address part even when I already created a document in
products
collection of IDshipping_countries
per extension instructionYou may have to remove the config
STRIPE_CONFIG_COLLECTION
in the config file (if you are using Emulator) or the "Stripe configuration collection" if you are using real life server. The reason is because in the extension coding we haveconst shippingCountries: Stripe.Checkout.SessionCreateParams.ShippingAddressCollection.AllowedCountry[] = collect_shipping_address ? ( await admin .firestore() .collection( config.stripeConfigCollectionPath || config.productsCollectionPath ) .doc("shipping_countries") .get() ).data()?.["allowed_countries"] ?? [] : [];
This means if you have the config for
STRIPE_CONFIG_COLLECTION
, it will overwrite the config location forPRODUCTS_COLLECTION
config and make it unable to retrieve the information about the shipping