Build a GraphQL Mutation to Save Bookings and Test Stripe
In this lesson, we'll wrap-up what we've done in the last few lessons by now collecting all relevant information and triggering the `createBooking` mutation when a user is ready to confirm their booking.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
[00:00 - 00:17] With our Listing Create Booking model prepared and presenting the payment element, we'll now focus on executing the Create Booking mutation when the user provides their payment information and is ready to book. We'll create the mutation document in our Lib GraphQL mutations folder.
[00:18 - 00:39] We'll first create a Create Booking folder that is to have an index file. In the file we'll import the GQL tag from Apollo Boost, and we'll construct a constant called Create Booking, that is to be the GraphQL mutation document.
[00:40 - 01:02] We'll name the mutation of the same name and it would expect a required input of GraphQL type Create Booking input. We'll also specify the mutation field we'll want to run, Create Booking.
[01:03 - 01:29] With regards to what we want to return from the result, I don't believe we'll need much returned since we will simply close the model in the UI and tell the user that the listing has been booked. We don't actually need any of the data to be returned. However, in this case, we'll just have the ID of the booking result be returned and we may not even use it.
[01:30 - 01:54] And in the mutations index file, we'll react sport the Create Booking mutation documents. We'll then head over to the terminal and in our client's project run the Code Gen schema command to update the schema file.
[01:55 - 02:37] When complete, we'll also run the CodeGen generate command to auto-generate the GraphQL typings for our new mutation documents. We'll head over to the Listing Create Booking model file and we'll import the Use Mutation hook.
[02:38 - 03:18] And the data and variable typings for the mutation associated with it. And since we'll need it shortly, we'll also import the Display Success Notification and Display Error Message functions we have in our libutils file.
[03:19 - 04:02] In the beginning of our components function, we'll declare the Use Mutation hook, specify the shape of data and variables associated with the mutation pass in the mutation document and we'll only destruct the mutation function and a loading property from the mutation results. In this component, we already have a function called handleCreate Booking that we've set up in the last lesson that would run when the user clicks the Book button.
[04:03 - 04:27] And we have the injected Stripe Prop Object available that has details on the payments information the user can provide. What we'll do in the beginning of this function is we'll check and say that if this Stripe Prop Object doesn't exist for some reason, we'll simply return early and run the Display Error Message function and say, "Sorry, we weren't able to connect with Stripe."
[04:28 - 04:45] From the Stripe.createToken function we had before, we're also able to destruct an error property as well. This error will be populated based on invalid payments information provided in the Stripe element.
[04:46 - 05:09] We'll then check and say, "If this Stripe token is available, run the Create Booking mutation and will provide values for the variables that we can." In the Input Variable Object, the source will be the ID from the Stripe token.
[05:10 - 05:28] The Check In and Check Out variables expect the dates in String format. We have the Check In and Check Out date state properties from the parent passed into this component, so we can simply use the moment function to format the date objects as strings in the format that we'll want.
[05:29 - 05:38] We'll select the number of digits for that, month to digits for that, and day to digits for that. This is the format we'll send it to the server.
[05:39 - 06:01] If for some reason the Stripe token doesn't exist, this probably means that the user has either provided invalid payment information or hasn't provided payment information appropriately. We'll run the Display Error Message function and we'll check to say, "Does this error property destructed from the Stripe token function exist and does it have a message property within?
[06:02 - 06:14] If so, we'll have it be the message we send and show to the user. If not, we'll just show a generic error message of, "Sorry, we weren't able to book the listing, please try again later."
[06:15 - 06:33] We're still missing a property for the input variable which is ID, and this is to reference the ID of the listing being booked. There's a few ways we can get this information.
[06:34 - 06:48] One way would be to use the React Router capabilities to access the match parameter in the route like we've done in the parent. However, in our parent, once the listing is determined, we have a listing data object that's been queried.
[06:49 - 07:08] So we can simply look to pass the ID of this listing object as a prop called ID . There's two other things we'll want to pass in from this parent down to the mod al child component, and they involve what we want to happen in our listing UI after the booking is made successfully.
[07:09 - 07:40] When the booking is made successfully, we'll want the modal to be closed, and we'll want the input values in our check-in and checkout inputs cleared out. The state values in this parent listing component determine this, so we'll prepare a function in this parent listing component called "Clear Booking Data" and it will be responsible in setting the state values that we want back to their predefined states, so we'll run the set modal visible function and pass a value of false.
[07:41 - 07:49] This would close the modal. We'll run the set check-in and set checkout functions and pass values of null.
[07:50 - 08:05] This would clear out the input values in the date picker inputs. And lastly, we'll pass the clear booking data function as a prop down to the modal component.
[08:06 - 08:26] A slight misspelling, we wanted clear booking data, not clear booking dates, so we'll update where we have it defined to clear booking data. When the mutation is complete successfully, we'll also want the listing information presented to us in this listing page updated.
[08:27 - 08:36] But what part of this page would we actually need updated after the mutation is complete? Primarily, it would be the Bookings Index of the listing.
[08:37 - 08:50] Remember that in our listing Create Booking component, we attempt to disable the dates in the date pickers where the dates have already been selected. And we declare and reference this from the Bookings Index of the actual listing .
[08:51 - 09:10] So essentially, when the user is to make a booking, and if they were to remain in the page without refreshing, by opening the date pickers again, they should see the dates that they've recently booked disabled in the date picker. We did mention a little while ago how Apollo directly gives us the ability to update the cache.
[09:11 - 09:19] But in this context, I don't think it would be preferable to do so. One reason why I don't usually try to update the cache is it isn't that intuitive.
[09:20 - 09:41] And in this case, however, the second reason is, though we primarily are concerned with updating the Bookings Index value of the listing object, there are other fields within the listing document that gets updated. So out of preference and since it would be simpler, it might just be easier to simply refetch the listing data in the listings page.
[09:42 - 10:01] We just need to make another network request after the booking is made successfully. So with that said, we'll destruct the refetch property from the use query hook in the listing component, and we'll create an asynchronous function called handle listing refetch that simply is to run the refetch function.
[10:02 - 10:15] And we'll pass the handle listing refetch function down as props to the modal component. In the modal component, let's define the new props it is to accept.
[10:16 - 10:39] The ID of type string, the clear booking data function that returns void, and the handle refetch function that returns a promise when resolved is void. Handle listing refetch returns a promise since it's an asynchronous function.
[10:40 - 10:57] I believe the other area where we've done something similar to this is in the user page. However, in the component that accepts this function, we've simply had it to return void.
[10:58 - 11:13] And though TypeScript doesn't really complain here, to be explicit, we'll say it is to return a promise that when resolved is void as well. In the component function, we'll expect and declare the props that it is to accept.
[11:14 - 11:31] ID, the clear booking data function, and the handle listing refetch function. When we run the create booking mutation function, we'll now pass in the ID that 's available as props.
[11:32 - 11:46] And we'll now also look to handle the success and error states of this mutation , and we'll use the uncompleted and on error callbacks of our mutation options. For the on completed callback, we'll clear the booking data and the parents.
[11:47 - 12:27] Display a success notification with a title of "You've successfully booked the listing" and a description of "Booking History can always be found in your user page" and we'll then run the handle listing refetch function to refetch the new listing data. For the on error callback, we'll simply display a generic error message of " Sorry, we weren't able to successfully book the listing. Please try again later ."
[12:28 - 12:57] And lastly, we'll use the loading property from our mutation result and place it as a value for the loading prop of our actual book button. Okay, let's now try out what we've done so far. We'll go to our application.
[12:58 - 13:10] And in this case right now, I have two separate pages open. And in this case, the reason why I'm doing this is I'm going to have two separate profiles with two different roles in the example use case we're going to go through.
[13:11 - 13:26] With the original profile I have, Hassan DJ with the contact of my email, Hassanjurdeijima.com, I want to have this profile at this moment play the role of the host. But that person who has a listing where somebody else is going to be booking on that listing.
[13:27 - 13:44] And as a result, I've needed to connect to a stripe and I am connected and I already have a listing created. On the right hand side, this particular profile here with my second contact email, Hassanjurdeijima.com, I'm going to play the role of the tenant or the person who's interested in making the booking.
[13:45 - 14:00] So in this instance, I want this user on the right to book this particular listing we've created before, the large Bel Air mansion in LA. So I'll search for LA, I'll look through, and I'll find this particular listing .
[14:01 - 14:13] We see that this listing's owner or host is Hassan DJ and the description on this listing is shown like we've talked about before. And in the date picker section, we're able to select the check-in and check out days that we want.
[14:14 - 14:19] And we're notified that we won't be charged yet. And the price for this listing is $200 per day.
[14:20 - 14:34] So I see you might want to check in on a Thursday and I want to check out on the Friday right after. And if we always count the check-in day as the day being booked as well, that will be two days where the day is being booked.
[14:35 - 14:40] In this context, we'll pay $400. And the stripe element is not presented to me.
[14:41 - 14:53] This is where we can try out to see what if we try to book something here where this stripe element isn't given the correct payment information. So if I click book right away, I'll see the message that says your card number is incomplete.
[14:54 - 15:02] Where is this error message coming from? We check in our function that does the stripe token exist. And if it doesn't exist, we automatically display an error message.
[15:03 - 15:18] When the stripe token doesn't exist, it usually has an error property attached from the stripe payload that often tells us that something is missing, which is why we displayed that particular message. And we've talked about before how stripe gives us a large number of different test payment cards.
[15:19 - 15:31] We'll stick with using the 424242 option all the way through, which is a valid use case. So this 424242 all the way card number represents a Visa card and the test environment setup.
[15:32 - 15:50] Now, if I don't provide the three digit CVC code and I click book, I'll be warned that says your card security code is incomplete. If I do provide the CVC code of just three random digits in my test environment , and for some reason I click let's say an expiry date in the past.
[15:51 - 16:00] Oops, that is not the past, let's say 11. In the UI we're notified that it's incorrect, but if I click book, I'm also told that your card's expiration year is in the past.
[16:01 - 16:16] Okay, so let's pick some random data into the future, 0424 and the CVC code would be 744. If I click book, let's see what would happen. The mutation is run, the loading indicators run, the modals closed and we get the success notification.
[16:17 - 16:30] You successfully book the listing, booking history can always be found in your user page. Amazing. Now at this moment, there's quite a lot of different things that happened and we want to verify that they've all happened the way we wanted to.
[16:31 - 16:44] When a booking is made, we'll want the host, the person who owns the listing to receive the payment that we've specified in the model. Essentially, the person making the booking should pay the host in our example $ 400.
[16:45 - 17:06] We, Tiny House, should receive 5% of the payment charge. The reason being is where the marketplace that owns this application and for the host to host their listing on our app, they should pay a small percent to us. In the user page of the tenant, the person making the booking, the booking should then be shown in their booking list.
[17:07 - 17:23] In the user page of the host, their income within the user profile should be updated because they've now gotten paid. In the listing page, the recently booked dates should be disabled in the date pickers.
[17:24 - 17:42] And lastly, in the listing page being booked from the viewer perspective, only the viewer would be able to see this, the booking history should be shown and should be notified who's made the booking. So with that said, let's assert that the Stripe payment charge worked as intended.
[17:43 - 17:52] And to do this, we'll first need to look at our Connect dashboard. So this is the Stripe dashboard for our actual Tiny House connected app platform.
[17:53 - 18:03] And we can see while looking at viewing test data, that the Connect grows volume today was $400. And there's one new connected account.
[18:04 - 18:10] And we've collected $20 as the application fee. Why $20 in this context?
[18:11 - 18:16] This is because we charge 5% of every payment made. $400 was paid out.
[18:17 - 18:20] We take $20 from that. Amazing.
[18:21 - 18:31] And if we looked at the Connect link on the left-hand dashboard here, we'll see the accounts that have been connected to our app. Now, keep in mind that there's something here that we should explain.
[18:32 - 18:39] Technically, we should only be seeing the hasandajurdieajima.com account, right ? The second one here can be ignored.
[18:40 - 18:47] That was something created before this particular lesson. But the recent connected account that they're referring to is actually the one above here.
[18:48 - 18:59] Account 1 FMAD. Now, why is this having a new account ID being specified here as opposed to simply using the hasandajurdieajima.com account that exists before?
[19:00 - 19:13] This is primarily due to how we're actually connecting to our application. When we connect with Stripe, we're given the option to either skip the account form and/or log in with an actual Stripe account.
[19:14 - 19:34] By not logging in through a Stripe account and by simply skipping the form, what the Stripe Connect platform does is it automatically creates an account ID for this fake account to represent what this Stripe account looks like. Which is why at this moment, if I actually looked through my other Stripe account, which is to represent the host, I'll see that no payments have been made to me here.
[19:35 - 19:43] Because in our test environment, we haven't appropriately logged through with this Stripe account. So with that said, let's try something else.
[19:44 - 20:02] If we go back to our connected account, hasandajurdieajima.com, and assume that we disconnected from Stripe, and we want to connect again. By connecting again, it will tell us that we perhaps are connected elsewhere and in this case, it's referring to actually my browser session, referring to the actual tiny house connected platform.
[20:03 - 20:12] So in this case, I'll log out and reload. And by doing so, it would give me two options for me to either skip this account form or for me to actually sign in with an actual Stripe account.
[20:13 - 20:25] So instead of just skipping the account form, let me try to sign in with an actual Stripe account. I'll provide the account that I'm interested in, hasandajurdieajima.com, and is the appropriate password.
[20:26 - 20:43] And in this case, it tells me that I'm logged in as a signjurdieajima.com. So when I skip this account form, I'm hoping it references the actual logged in account as opposed to simply creating a new Stripe account ID.
[20:44 - 20:56] Great. In this moment, though we want to verify this in steps later on, we do see the income this account has earned $400, which was paid out before.
[20:57 - 21:10] But we want to ensure that within the Stripe Connect platform that it understands that this account references this email and not simply another Stri pe account. So we'll go back to the other user, which references the person who's about to make the booking.
[21:11 - 21:22] And in this case, let's assume we want to book another date. Another thing we can quickly verify here, is we can see that the dates we've booked before, Thursday and Friday, have already been disabled in the date pick ers.
[21:23 - 21:33] So let's pick the two other days in the future, Saturday and Sunday in this context, the 14th and the 15th. And the amounts we'll pay here will be the same since we're booking two days.
[21:34 - 21:41] In this case, let me provide the card number. And just to ensure that we can get through this, I'll click Book.
[21:42 - 21:50] And when successful, I'm given the notification. So now with that said, let's see how our Stripe Dashboards look.
[21:51 - 22:08] In the parent connect account for Tinyhouse, just simply in the home section, we'll see that we've grossed today $800 and we've collected from that $40. And there's been another connected account within the last week or so, which is today.
[22:09 - 22:34] And now if I go to the accounts section of my Connect tag on the left hand side , I'll see that no other wallet ID or Stripe ID was connected. However, the balance for the original account that we've been referring to, As ai and JURDAJIMA.com, looks like it is increased, but it's hard to tell here, which is why I will now head over to the Stripe account dashboard for disconnected accounts.
[22:35 - 22:40] Asai and JURDAJIMA.com. There's a lot of other test payments we've made before from May and before that .
[22:41 - 22:51] But today's date as of recording the screencast of December 11th. And I can see here that I've gotten $400 for this particular account.
[22:52 - 23:04] $400 is USD and it's succeeded. Amazing. Now if I go to the home of this particular dashboard account, I'd also get some information of what I've made within the last week or so.
[23:05 - 23:12] However, in this case, it would tell me that I've grossed $518. The reason being is this account is in Canadian format.
[23:13 - 23:17] I live in Canada. So any US dollars that come through comes in Canadian dollar format.
[23:18 - 23:29] So in this context, I've netted from gross 400 USD, which comes up to be about 518 Canadian. However, more importantly, the one section that I want to refer to is the net volume for sales.
[23:30 - 23:35] It tells me what the gross volume was, but we don't make all this amount. We make what we net.
[23:36 - 23:56] And the reason being is we net whatever after the payments disconnected account automatically pays for in terms of the application fee, connect transfers, the stripe fee and so forth and so on. So through within this test environment, this particular host account has made 474.48 after netting.
[23:57 - 24:13] So one important thing to keep in mind is whether you disconnect and connect with Stripe. If you want to have the Stripe connected account referred to a certain account, be sure to log in in that particular page with that Stripe account and then skip the actual form.
[24:14 - 24:33] In that context, that connected account will refer to a single one as opposed to Stripe simply creating a new fake Stripe user ID for every time you connect. With that said, now let's look to verify the other things that we want to ensure should have happened after an actual connection was made.
[24:34 - 24:45] Now on the left hand side, one thing we mentioned was the income should be updated to show the total gross income a host makes. Now this was a separate browser tab, so we'd probably have to refresh it regardless.
[24:46 - 25:01] And we can see now here we have an income that was earned that was $800. Why? Because it refers to now the total amount that we've just paid out, which in the user on the right is made two payments for $400 each.
[25:02 - 25:09] In the user page of this tenant, so let's go to the profile page of this particular tenant. At this moment, we see nothing here.
[25:10 - 25:29] This is because everything within our actual UI for this page is cached. We'll talk about some of the improvements we can make to get network information automatically, but in this case, if I refresh the page now and I scroll to the bookings, we'll see that the two booking moments that I've specified is now being reflected here.
[25:30 - 25:54] And I also see the dates that this user, the tenant has requested to check in and check out 12th and 13th as well as the 14th and the 15th. We've quickly seen this before as well, but in the actual listing page, at the moment, the actual booking was made and was successful, the dates that have been booked will now be disabled in both of the date pickers.
[25:55 - 26:06] This tells us that these dates have already been booked. And lastly, the one other thing we should look for is in this context, when I look at the listing page as a tenant, I don't see the private booking information.
[26:07 - 26:32] Now on the left hand side, as this user, if I go to the listing page, I would actually get some of the booking history for this page since this viewer on the left hand side owns this listing. And I'll see the profile image, which has a link to the user page of the actual person and the check in and check out dates for where this person wants to check in and check out.
[26:33 - 27:03] And one thing I do want to mention is for our actual stripe payments that the tenant pays out to book a listing, we've kept things simple and we simply said we want to capture a user's credit card or debit card number and their expiry date and CVC code for their payment card. And in the application you're building, there may be other information you need to capture, maybe the user's address or postal code, etc. And the stripe elements provides all those different UI elements for you to use.
[27:04 - 27:20] Regardless, with the way we've done it, the outcome would be the same. We simply would receive a stripe token, thanks to the stripe element, and the stripe token will provide the source of the payment that's been made, and we'll be able to check if that payment information is invalid or valid.
[27:21 - 27:26] And that's pretty much it. The main core functionality of our application is now complete.
[27:27 - 27:51] Users are able to view listings, users are able to book new listings, users are able to create listings for the users that book listings, they pay out the person who owns the listing, and we tiny house simply receive a small 5% charge of the payment, and the connected user account pays that out to us. All the main core pieces of our app is now complete.
[27:52 - 28:08] Fantastic job so far. In the next coming modules, we'll address deployment and how we can have our local development app now hosted with the help of Heroku, and then we're going to have a module focused on primarily just small improvements we can make to our app.