Jacqueline Nolis | I made an entire e-commerce platform on Shiny | RStudio (2022)
E-commerce requires passing data between many components like managing a shopping cart, taking payment, fulfilling orders, and sending emails. I've successfully created a full e-commerce platform entirely in R for a quirky side project. The R package ggirl lets users order ggplot2 plots as postcards and more via R functions. Those R functions pass data to a separate Shiny app, which then passes data other services like Stripe payment APIs and printing APIs. In this talk I will walk through how to use packages like httr, callr, and brochure to have your Shiny apps call external services and do many tasks in parallel. You’ll leave the talk with more ways to use Shiny than dashboards plus the knowledge to monetize your existing dashboards! Talk materials are available at https://link.jnolis.com/rstudio22-slides Session: Unexpected uses of R
image: thumbnail.jpg
Transcript#
This transcript was generated automatically and may contain errors.
I'm Jacqueline Nolis. This is my presentation how one time I made an entire e-commerce platform on Shiny. They said it couldn't be done, but I did it.
What is that platform? That platform is ggirl, which is to make your ggplots in real life. Some people may pronounce it ggirl, but it is officially ggirl. You can install it via remotes, and it lets you take a ggplot you've made that's beautiful that you wish existed in the physical realm. You can then pay a modest fee, and very soon you will get an actual postcard delivered to a friend or family member.
But it's all cool to talk about it. Let's actually see how it works instead. So I'm going to immediately switch out of PowerPoint and switch to a live demo. So I'm running this in Saturn Cloud. I'm just going to open up RStudio.
So you have this souvenir commemorative GGIRL plot. You can then go and you can use this GG postcard function, which takes a plot, the contact email for the order confirmation, the message you want to put on the back of the postcard, like this is a great conference, thanks for putting it on, and the address of a friend or family member. In this case, I'm going to send it to our friends at RStudio, I mean Posit, in Boston, Massachusetts. And in a couple of weeks, their headquarters is going to be confused why they got a postcard.
So I'm going to hit that, and what's going to happen is it's going to think for a second, and then it's going to launch an entirely separate Shiny server. This is a Shiny server running somewhere else. It is not running on my computer. It is running actually on Saturn Cloud.
So on the front, you can see the preview of the postcard. It shows you the message. It shows you the address. It has one button, and that's pay and submit. When you hit pay and submit, there we go, so when you hit pay and submit, it thinks for a second and sends you to Stripe. In Stripe, it asks you for money. So you do that, and then if you did somehow put in your credit card information in front of a bunch of people, you would then very soon get an email confirming that you made the order. In one to two weeks, you will get a physical postcard of your Gigi plot.
How it works: architecture overview
So that was the demo. As you saw, it went from R to a shiny app to Stripe, and then somehow a postcard appeared. But how does it work?
So there's an R package called Gigi IRL. That's what you install on your computer and talks to Shiny server. And then there's a Shiny server called Gigi IRL server, which does all the work of showing the user preview, passing the information to Stripe, fulfilling the postcard via an API, so there's a service somewhere I call to make the postcard go, and then sends a confirmation email.
Now, if you think about it, Gigi IRL server should be multiple Shiny apps and plumber APIs. There's no reason why the fulfillment, why the uploading the information should be done via Shiny. It's probably better to do that as an API via plumber. But for ease of maintenance, I'm like, I really just want one Shiny app to do all of this. And by ease of maintenance, I mean I'm super lazy, and I don't want to have to keep a bunch of microservices running. I just want one.
Passing data from R to Shiny
But to make this work leads a lot of engineering questions, which I'm going to enumerate now. One is how do you pass the plot from R to Shiny? If you think about it, Shiny, there's a lot of times where Shiny has data loaded into it, right? You can read data from a database in Shiny. You can press the upload button on Shiny. But if you think about it, there's not really an obvious way that if a Shiny server is running, how can you programmatically send data to it?
So the way you would do that in, like, web development land is you do an HTTP post request. So an HTTP post request is a way an API or a website receives data. So when you hit submit on a website, your browser is doing a post request to the server. That should be done with a post request. Shiny does not have a documented ability to listen for post requests. Normally only Plumber does that. It should have this. And that's my call out to RStudio. I mean, pause it. I think that would be a really great feature.
Shiny does not have a documented ability to listen for post requests. Normally only Plumber does that. It should have this.
The good news is there's actually a number of undocumented ways to do this. And so, again, what I'm trying to do is I'm trying to post the image to Shiny. So here's the plot I've made. And then I want Shiny to come back to me with, okay, here's the ID of that image. And then the package is going to be like, okay, cool. I'm then going to take the user to the browser website to actually do the preview. So this is a back and forth of the post request.
So there are a number of undocumented ways to do this. The way I like the most is you use the brochure package by Colin Fay. This is an experimental package that lets you connect multiple Shiny apps together. So if you have like three different Shiny apps and you're like, man, I really wish all of them were running at once, you can bundle them all up into one Shiny app using brochure. You can also listen to requests besides get. So if you want to have your Shiny app listen for data, not just show webpages, brochure will let you do that too.
So this actually solves two of my problems. One, it lets me listen for post data. And two, I can actually have a bunch of concurrent Shiny apps, one for the order upload, one for the fulfillment, one for the preview. It's so new, it doesn't even have a hex sticker. It also says all over it, experimental, do not use in production. But here I am using in production and it's working great. Thanks, Colin.
Showing the image preview
So how do you show the image preview? So once we have the data uploaded into Shiny, how do we show the image? Well, this is actually just a Shiny app with one button. What I do is I have the URL you go to, have the token, which is the ID of the particular postcard you uploaded. I have that in the URL and then the Shiny app shows a different postcard depending on what your token is.
Now, I put a lot of work into the bootstrap and the CSS to make it all look pretty and to get the margins on the postcard right and all that. I have a whole separate talk about making your Shiny apps look pretty that I gave at ShinyConf 2022. So if you want like 45 minutes of hearing about how to make your Shiny apps look pretty, I suggest you check out that talk.
Passing the user from Shiny to Stripe
So, okay, cool. So we have a Shiny app. It shows you the preview of the thing. It got the data from a post request from R. But how do we then pass the user from Shiny to Stripe, right? How do I make that pay and submit button send someone to Stripe? I thought this would be the hardest part of the app, but it actually was surprisingly easy to make this.
There are really just two steps in the payment flow. First is you need Shiny to send Stripe what is actually in the shopping cart of the user, which is called the Stripe session. You do this via a post request. By the way, it turns out GGIRL, it's like 95% just post requests bouncing around, and this is another one.
You then also, once you've posted the cart, you then have to have the user go to the Shiny page or the Stripe page. So Stripe has a JavaScript API to get users to the appropriate Stripe checkout page. I refuse to learn JavaScript. I will never do this.
The good news is that Shiny makes it very easy, I found out, to use other people's JavaScript APIs. So what you do is you add tags, $script to the top of your Shiny app to load in the JavaScript API, and then you Shiny.customMessageHandler so that when the action button is pressed for pay and submit, it will call that special JavaScript. So it's really just two steps.
Fulfilling the order with webhooks and callr
How do you then fulfill the order? So how do you have Shiny then listen for Stripe, Stripe being like, okay, cool, they paid you money. Now we're going to go ahead and make the order. So we do this with a POST request. So we're going to have Stripe send a POST request telling Shiny to order the postcards. So if your server is listening for someone else's POST request, that is called a webhook. It's just a different name for another POST request. It's just kind of going in the other direction.
So we're going to use a webhook saying, hey, order XYZ or FCE6, whatever the order number is, has been paid for. And to make sure that people just don't know what that URL is and spam it to get a million postcards, we are going to add keys to ensure that the message actually came from Stripe itself.
So Stripe will then, so Shiny will receive the request and be like, okay, cool, the order is placed. And then Shiny will call the stuff to send the postcard and send the email. So, and this feels like it should be straightforward, right? I've already done two or three POST requests as part of this talk. This is just another POST request.
But here's the issue, is that Stripe needs to know that the message was received really quickly. So if Stripe says the order was confirmed, and Stripe doesn't hear right away that the order actually, like, you're Apgil, like, cool, we got the order, she'll be like, oh my god, they didn't hear it. I got to send it again. And here's the thing you might know about Shiny, is Shiny takes a while when it's running stuff. But if you're not careful, Stripe will ask for the order to be submitted over and over, and Shiny will be like, cool, got it, cool, got it, cool, got it. And the next thing you know, you have 20 POST cards sent to your house.
Stripe will ask for the order to be submitted over and over, and Shiny will be like, cool, got it, cool, got it, cool, got it. And the next thing you know, you have 20 POST cards sent to your house.
So the trick to solve this is you're going to use callr package. And callr's a really cool package to run our processes in the background. So when you get the order from Stripe, you're going to use callr to send, like, actually send the POST card, send the email. That's all going to happen in the background, so that Shiny can immediately respond to Stripe being like, yeah, cool, we got it, and not be bogged down by the time it takes to submit the POST card order and send the email.
Printing, emails, and deployment
So to actually fulfill the order, I needed to use callr to run R in the background. And then to order the POST cards, it turns out there are, like, tons of companies out there that will make APIs that if you hit that API with a POST request, with the message, the address, and the front of the POST card, they will ship it to that location. This makes sense because you get lots of POST cards in the mail for advertisements. So lots of companies use these services.
Two problems here. One, it takes a while to ship POST cards, right? It takes, like, two weeks to ship POST cards. So it takes a while before you actually get the POST card back. And two, companies that are sending millions of POST cards to every person in the country don't care really how good the POST cards look, as long as they're cheap. So it took forever for me to find a service that was actually good. That is beneath the dignity of ggplot2 plots. So I had to spend a while. By far, this is the longest part because I had to spend a while until I find a printing service that was actually good.
To send the confirmation emails, this is actually pretty easy. There's a package Blastula, which can send those emails. So all I had to do to put the preview in the email is I wanted to resize it because those images had to be very large for the printer, and I wanted to resize them so they weren't like five megs in your inbox. So I used the package ImageMagick to resize them and get the right margins and everything.
But sometimes there would be an error in the fulfillment. So not only did I have to send an email to the users, but if anything ever goes wrong, I wanted to make sure I caught it, so I'd have to send an email to me. And once in a while, the email system would have an error in emailing, so I had to email about that too. It was a real web of emails being sent.
So lastly, how do you actually deploy it? So I needed the server to actually be... That ggirl server, the Shiny app, had to be continuously living somewhere. So the place it is currently running is on Saturn Cloud. So Saturn Cloud, I'm the chief product officer of the company. So it was actually very nice because I had two RStudio servers running in Saturn Cloud in the cloud. So one was the one I actually was showing you, which is how I did the development of the Shiny package in RStudio in a browser. But I also had a RStudio running for developing the Shiny backend. And I actually had two Shiny servers running on Saturn Cloud.
One, the actual server, the Shiny server you saw, the production one, but as well as a development one that I could test with and use like fake credit cards and it would fake send emails. So I ended up having four systems all running on Saturn Cloud, which is pretty nice and easy. And most importantly, I beta tested this with a bunch of people to make sure it worked first. And that took a while and helped me really iron out some usability issues.
Wrapping up
So wrapping it all up. ggirl is a cool package for ordering cool stuff. This required me to do a bunch of things that I assumed might be true in Shiny, but I didn't actually know, and it turns out it is, which includes receiving data from an R package, listening for post requests, sending and receiving Stripe API calls, and spawning background processes that didn't slow down the Shiny responsiveness.
I forgot to mention at the beginning that not only can you send GG postcards, but you can also order art prints for your walls, which people have used for graduation presents and stuff, which is really cool. And people use, oh, by the way, people use GG postcards for baby announcements and stuff. People get really clever uses for these, so it's a lot of fun. And there's GG watercolor, which is just, I hand-paint a watercolor for you if you want it.
So you can install the package with the ggnolist, ggirl. You can actually, if you want to make one of your own souvenir commemorative postcards, the code's linked above. The examples of the code in practice from the production server are at the ggirl examples. And if you want to see the slides, that's the link. But lastly, I have a whole bunch of coupons for 80% off, which means you can get a postcard sent for $0.50, which is the cheapest Stripe will allow me to do before saying I can't do that. So if you come talk to me after, I have so many of these coupons to give out because I'm really excited for people to use this. So, okay, thank you.