Subscription Management in Jamstack Apps
Subscriptions power most web-based businesses. In this episode, Thor 雷神 teaches us how to let customers purchase & manage subscriptions with Stripe in Jamstack apps.
Links & Resources
- https://docs.netlify.com/visitor-access/identity/?utm_source=learnwithjason&utm_medium=stripe-subs-jl&utm_campaign=devex
- https://fauna.com/
- https://stripe.com/docs/billing/subscriptions/integrating-self-serve-portal
- https://github.com/netlify/netlify-identity-widget
- https://docs.netlify.com/functions/functions-and-identity/?utm_source=learnwithjason&utm_medium=stripe-subs-jl&utm_campaign=devex#trigger-serverless-functions-on-identity-events
- https://www.npmjs.com/package/stripe
- https://cli.netlify.com/commands/deploy
- https://jwt.io/
- https://developer.mozilla.org/en-US/docs/Web/API/Location/href
- https://github.com/stripe-samples/checkout-netlify-serverless
- https://thorweb.dev/
Full Transcript
Click to toggle the visibility of the transcript
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
JASON: Good morning, everyone. And welcome to another episode of -- whoa! Hold on! What did I do? Whoa! Eh! Everybody okay? Sorry about that. Had a window open. Definitely didn't have it muted. Let's try that again. Good morning, everyone, and welcome to another episode of Learn with Jason. Today on the show, we're bringing back Thor. Thor, how are you doing?
THOR: I'm great. Thanks for having me back.
JASON: So, if you have been watching the show for a while, you've seen Thor a few times. We have been doing all sorts of fun content around Stripe and the JAMstack. We have built a products page. We have built donation pages. We've set up the ability to do shopping carts. And Apple Pay and Google Pay. And all that's being handled on the JAMstack, which is really fun. We used some serverless functions, Stripe to manage the payment details and security. And that leaves us with the ability to very easily stand up something that will take money for us. Without having to do a bunch of servers, without having to do a bunch of security. Today we're going to do what I think is the most ambitious thing we've tried so far. We're going to set up a full-on subscription app with multi. Subscription tiers. Starting with free, going to a basic plan and a pro plan. And we're going to control access using Netlify identity and function. Today we're going to put together subscription payment management, user login and authentication. We're going to put in access control. It's all coming to the. Okay, so, before we get started, Thor, do you want to give us a quick intro to who you are, what you do? All that stuff?
THOR: yeah, sure. I'm Thor, a gorp advocate at Stripe. And I have been hanging out with Jason and doing fun JAMstack stuff, yeah.
JASON: Yeah. I think you have been the person I've met the most time with since quarantine started.
THOR: And we have yet to meet in person.
JASON: I'm coming straight to Singapore. I want to eat.
THOR: Yes.
JASON: One note for the chat, if you have not been with us since yesterday, we have support for live captioning. Go check out LWJ.dev/live to get live captioning. LWJ.dev/live, I need a command for if. Thanks, Ben, for beating me to the punch. What about Marissa? True. Doesn't count. Go there, we have White Coat Captioning. Amanda's helping us out today. It's amazing that they're able to be here. That is something that, you know, it's a premium service. Can that's being made possible by Netlify, Fauna, and that almost covers the bill. The rest is coming out of my pocket. If you are a company that would like to sponsor stream, please let me know. We are going to use Fauna today. We are going to use Fauna to store our subscription details and also to store our content. Because we're gonna be -- basically we're gonna like, make requests for privileged content and we're gonna check that someone has the appropriate subscription level before we return that content. That's how we're gonna protect it. So, yeah, Thor. What -- I mean, is there anything that you want to talk about in the abstract before we -- before we flip over to code land?
THOR: No. I think this is really exciting. I called it we're assembling the Avengers of JAMstack. So, that's kind of how I like to think about today's episode.
JASON: Yeah. Well, so, you're clearly Thor. I -- I'm -- I don't know. The bald one. [ Laughter ]
THOR: I meant more kind of Gatsby, Fauna, Netlify.
JASON: Oh, I understand.
THOR: Stripe.
JASON: I thought you were talking about us.
THOR: No, we're like the -- I guess the techie -- the techie guys in the background that, you know, make the dream happen.
JASON: Yeah. What's the -- there's -- I forget what her name is in the Avengers series. But it's like Robin from How I Met Your Mother. That's me in the background pulling all the strings making everything work. Let's maybe switch over and I'll talk away from this terrible metaphor and we'll start building some actual code.
THOR: Let's do it.
JASON: Okay. Let me pull up a browser here. And we're going to head over this way. There we go.
THOR: And asking if the captions are going to be saved. I think, yes, they are going to be on YouTube later as well, right.
JASON: Yes. So, I've got support on the Learn with Jason site for saving transcripts that before now I haven't been able to get them set up. Now they will be stored along with the episodes on learnwithJason.dev. I'm going to learn how to turn them into actual closed captions. I think you can feed them into Twitch and they become closed captions through the video player. If I can do that, you can also get them into YouTube. So, I believe, like, the goal here at the end is that the captions will be available everywhere. Thor and Jarvis. I'll be Jarvis. Jarvis is dope. So, I have -- I have an empty browser, I have an empty terminal. I am ready to go. Where would you like to start?
THOR: Yes, I think let's start by just setting up a completely fresh Gatsby project. Maybe we can just use the Gatsby starter.
JASON: The -- maybe -- let's do the -- eh -- let's go from the empty folder because I feel like the Gatsby starter adds a few things that I don't want to deal with today. Make a new directory. And we will call this -- not my name. JAMstack subscriptions. It's earlier for me. We're going to see how well I can type at 8:00 in the morning. Call this JAMstack subscriptions. And yarn, why? Because I like the way it does a package JSON better. And then npm install Gatsby, React, DOM. That's all you need for a Gatsby site. And then I will open this up once we finish installing. Oh, translate into Spanish. This is very interesting. I don't know how I would -- yeah. That would be dope. I have no idea how to actually do that. But that's a wonderful idea. Maria Hill, yes, that's her name. Come on compooper. So, what's up, Jack? How are you doing? What are you gonna build with the JAMstack once you are able to do subscriptions? Like... see, there's not a link yet. This time we're working just offscreen. If you wanted to see the captions, the captions are at LWJ.dev/live. If you want this, use this one instead. Let's open up a terminal. All we have right now is a package JSON, Gatsby React and React dominant and node module. Create a Gatsby site. Source, pages, JS. And import React from React, do a home page. We'll call it index. And that is going to be, for now -- do build a website. Export that as our default. And then, if we can just start this thing. Let's see what happens if I run nullify Dev. I hope you work. Let's see. Oh, it didn't pick up -- didn't pick up Gatsby. So, I have to tell it null fie -- oh, it didn't pick up Gatsby because we don't have a Gatsby config. I'm just going to create a Gatsby config.js. And we will have that module.exports. It's going to export nothing for now. Now if I run nullify.dev, it should pick up Gatsby. Missing script, Gatsby. Okay. Whatever. We're gonna do it this way. We'll go, build and then we're going to set the command to be npm run build -- that's what we're going to run. We're going to set our publish directory to be public. That's where Gatsby puts things. And speakeasies now that's going to live in a functions folder. And because we need an actual build command, let's run some scripts. We're going to have a build script which will be Gatsby build. And then we'll have a develop script which will be Gatsby develop. All right? Try that one for time. There we go. And now we have a website! Ah-ha! Make that a little bit bigger so we can see. And that's it. That's a whole Gatsby website. Right? We can do anything we want in here now. We've got a pages directory, this index.js is a plain React component. Because it's in the pages directory, it gets created like a page. And so -- the other thing that's nice is this is running off of Netlify's Dev, which means that if we need to add environment variables, we can do that through the Netlify config. We need to set up a Netlify site to do that, but we're in a position now where we're basically able to run this as a production product. It's really nice. So, Thor, what should I do next?
THOR: Oh, yeah. Let's -- let's actually add a sign-up page. And you think we want to use Netlify Identity to get folks to sign up.
JASON: Jay. So, I feel there are a couple ways that we can do this. One of the ways that I was thinking of doing is basically to limit the number of pages. Maybe our sign-up page is our home page.
THOR: Okay.
JASON: Maybe one page for our first-level subscription and one page for our second-level subscription. Does that seem okay?
THOR: Yeah.
JASON: Okay. So, if we have -- just turn this into a fragment so that we can do a bunch of stuff in here. And let's -- let's say we're going to have a sign-up for premium corgi content. Okay. And then we'll say something like "Get your subscription today to access the goodest corgis/corgos on the Internet." Okay. We need to do some Netlify can have identity stuff. Let's look at that. There's a Netlify Identity widget that sw-yx made. And I think -- let's see. Let's use the widget. We'll use the full kind of drag and drop thing. So, to take a look at what this means, it is -- where does it go? Here's a demo. So, this will give us the ability to log in or sign-up. And we'll just use the plain sign-up of name/email. We won't use the extra sign in stuff today. So,let get this. And then also sw-yx is looking for maintainers. If anybody is looking to get into open source, this is a good project to take on. Let's get this -- let's get this widget. So, I'm going to add React Netlify Identity. Identity widget. And stuff from reach UI. Reach is what's under the hood for Gatsby for the router. So, this is nice and compatible. So, let's npm install all those goodies. And let's read about how we're going to implement it while it installs. So, I need an identity modal. We need to use the identity context. And then we need a context provider. And that's gonna come in from the Identity widget. We're going to pull on the styles and then pull in some Reach styles. Okay. So, let's -- rather than making me type all of that out, let's just bring that straight in. So, we've got our identity modal context, context provider. And I feel like we might need to put this part outside of the Gatsby page system. Which is -- that's okay. So, let's... hmm m. How do we want to do this? I want to wrap the root component. So, let's add Gatsby ssr here. And then we're going to export on wrap root element. And this is gonna send in element, I believe. And then we can just return the identity context provider around our whole app. So, if we import that, IdentityContextProvider from React Netlify Identity widget, yes. Then we can do IdentityContextProvider around the owner. Okay. So, that should work. We'll see how good my memory is. I haven't done this in a while. And then I'm gonna get Gatsby browser and we're going to do the same thing. But the bonus is we can actually import -- sorry. We can just export wrapRootElement from Gatsby-ssr. Because it's the same function. We just want to make sure that it gets done. So, now we're managing it in like one place. But we should get what we need there. So, we don't need this anymore. But in our off-view, we're going to get -- let's see. We need to get Identity. So, let's just do this. Identity = useIdentityContext. Then dialogue, setDialogue. Oh, yeah. Okay. So, this is gonna come -- the whole like JSON web token that we get back from Netlify identity. When you have a JSON web token, you are going to get a user object. And you will have user metadata and app metadata. User metadata is something I can update as a user, I want my user to be this or this to be this. And app metadata will be something like role. In our case, we're going to set roles of what tier you're subscribed to and that will control different parts of our app. So, let's just -- let's just dump what we get here. So, I'm gonna say, if identity and identity and logged in. Say identity and identity logged in. Right? Is logged in. Then we -- let's just dump, I think, the whole thing.
THOR: Jason, while you do this, I think I'm having some connectivity issues on my end. Which is surprising. But I'm just gonna quickly pop out and see if I can get a fix.
JASON: Okay. Yeah, no problem.
THOR: See you in a sec.
JASON: Switch over it here for a second while we do that so we don't have weird broken bits. So, let's get -- oh, yeah. So, MT Lando, the context provider, the reason that I'm putting the context provider in Gatsby-ssr and not in the layout, is that when Gatsby switches pages, it will reload the layout. For a context provider like identity, we don't want that to reload. We want that to be managed at a global level. So, we put it outside the pages. Which means that Gatsby won't actually reload it during navigation. And so, what we are going to do is actually turn it into like a wrapper around the whole app that never unloads. Thor, are you back?
THOR: I believe so. Yep.
JASON: Okay. Let's switch back here, then. All right. So, we -- we've got our identity. And if identity is logged in, I'm going to just dump the whole object. So, we'll show identity and then we'll just format that a little bit so that it looks nice. Let's then add -- let's see. We have all of this -- so, yeah. And there is a way to do this with no framework dependency. We wanted to use Gatsby today because Gatsby is such a popular way to build JAMstack apps and it's got a lot of things around routeing and stuff. I think we will follow this up most likely with a framework-agnostic. So you can see how it will work on any frontend that's JAMstack ready. We've got this dumped. What I need to add, though, is I need to add a login button. And our identity modal is going to be down here. So, I think I'm just going pull this straight over and drop it right here. And then we need to decide whether or not the -- am I just -- no. We don't want that. We want to make sure that the identity is -- or that the dialogue only shown when we need it. So, let's are grab it in useState, false. And then pull in useState. And then down here, we'll say, like, let's see. Not dialogue. Then we'll show... I don't know. Button on click equals set dialogue true. What this -- here. Here. Yeah. Okay. Let's see what happens if we run this. I feel like I didn't get everything. But we're gonna -- we're going for minimum. I think the example that we saw had a lot of -- a lot of stuff. It was showing names and those sorts of things. We don't want to necessarily do that. We want to make sure this is on. We're doing a mic tap right now. Is this thing on? It is not on. Unexpected token. So, I broke a thing. Unexpected use of name. React must be in scope when using JS -- oh. Yeah. That would make sense. So, in here, I also need to import React from React. Okay? That was thing one. What else did I miss? Unexpected use of name in the index file online 29. Here's 29. Ah. Okay. We don't -- we don't have that, so, let's just leave the name out. And we'll just do something like this. Actually, do we get a user? Because that would be cool. Let's -- ah, let's do that. So, we will -- that reload. Okay. We got closer. Something else happened. Invalid Netlify instance URL. Undefined. Okay. So, for Netlify Identity to work, we need to have Netlify Identity live for our site. So, ignore, so, that we can edit it. And I'm going to ignore node modules, I'm going to ignore public. I'm going to ignore cache. And I think that's everything that I need to ignore. So, let's -- let's get init. And then we can -- let's see. We've got the get ignore, Gatsby browser, Gatsby config, the Netlify tunnel, our source page. Everything. Nothing is going up that we don't want. So, let's submit and say, call this a work in progress. Because it's definitely broken. Okay. Then I'm gonna create a repo. This is using the Hub CLI. Okay. Now that I have that, I'm gonna get push, set my origin to master. And then we will Netlify init. I want to create and configure a new site. We're gonna put it on my team. We're gonna call this JAMstack subscriptions. We're gonna use npm run build. We'll set the output folder to public. Okay. So, that build is gonna fail. But what it is gonna let us do is it's gonna let us enable Netlify Identity. So, I'm gonna jump into the dashboard, go to Identity, and click enable. And now it should just work. So, let's grab this. Okay. And then I'm gonna go back into our code. And I believe -- where did he set... the URL? It was in context. IdentityContextProvider was here. So, let's go set that in our ssr. And we'll have to fix that later. But for now, this should be okay. And I'm going to run Netlify Dev. And we'll see if that lets us get further. What's up, Henri? Objects are not valid as React child. So, I missed a piece. In which step? Any context provider. All right. Let me look up Gatsby ssr, or wrapper element. Because I missed -- I missed the syntax, I think. Wrap root element. That is going to be -- ah. It's destructured. That's what I did wrong. So, if we go back here. I just need to destructure that out. And now nothing on my sleeve. Hey, hey! Look at that. So, we have a setup. It looks like I've already run a Netlify Identity instance at this local URL. So, I'm already logged in. Hooray! [ Laughter ] So, this is what the object looks like. We have a user that comes back. We have our endpoint token. Or our endpoint in general for Netlify Identity. So, this is the name of our site that we just created. And then we proxy in all the APIs.netlify/admin. We get back an access token. And take in access token over to JWT. io, then we can drop it in here and we can see that it sends in app metadata that we can't change. I signed up by email. And then we get user metadata. And it shows us our full name and the sign-up source. So, all good stuff. We have like user login. And then we get a couple other things here like are they confirmed? Logged in? Auth fetch, all that good stuff. So, good. We're in good shape. So, then if I click this button, I can log out. And then if I reload, I'm not there. Then I can log in. I think it's -- see if I remember the one that I used. That's not it. I might just need to sign up -- let's just sign up. And we'll say... oh, crap. What's the error? Blocked by CORS. do I need to add...
THOR: Hm.
JASON: External providers. I've done this before.
THOR: You have to set like a whitelist of domains.
JASON: I can't remember. Email address, active users. I wonder if this is a limitation of the React Netlify widget. Because I definitely just did this with the non-React version and it definitely worked. So, I'm wondering... maybe we -- maybe we do it framework agnostic? That might be -- because the nice thing about the framework agnostic version is it has very little code. So, we end up with including the widget. And then we can just kind of drop this in. And that's a little -- a little less intense. So, maybe -- maybe we start there. Because that way we're not gonna be -- basically, I don't want to fall down like is this Gatsby or React or Netlify hole? So, let's -- let's do that. Let's bail on -- let's bail on Gatsby. So, I'm gonna go back out here. Let's just get rid of it all. Get out of here. None of this. Get out of my house. And then I'm going go in here. And we'll have a -- this folder, I guess. We'll still have an npm-run-build, we'll still have that. I'm going to get rid of Gatsby browser. I'm going to get rid of Gatsby config.
THOR: Oh, the chat are pointing at a link. Local host.
JASON: Um, okay. Well, I think I may have prematurely bailed. But at this point, we've come too far. Because I already deleted all the source code.
THOR: Oh, no!
JASON: That's okay. It will go -- this will go pretty fast. Because I have very little to do here. So, what I can do is set up a index.HTML. And in my index.HTML, I'm going to say, login. And then I can go right to our code. If I get browse. And I can get the HTML that I wrote. So, I'm gonna get this page one and paragraph. And I'll drop this in. Okay. That's all good. Then I'm gonna go over to the Netlify Identity widget. I'm gonna grab this script. And we are going to drop it up at the top because we want to make sure that we validate identity before we do anything else. And then I'm going to grab this login button. And we'll put that down at the bottom. So, this is gonna simplify things a little bit. Let's have it do -- have it run from source for now. And then I'm gonna run Netlify Dev. Okay. So, we've got our -- see? This is what I was hoping for. It just gives us the -- the URL. So, we go HTTPS. And then set that. Failed to load settings from JAMstack subscriptions. What does that even mean? Why are you giving me CORS error? That doesn't make me -- you're making me unhappy. I literally just did -- oh. The site deploy failed. There is no site. So, it can't read the settings. Okay. So, that's -- that's fine. That is -- that's kind of an embarrassing thing to -- okay. Let's try this again. So, we've got -- we've removed the Gatsby browser, the get ignore, the package JSON. Let me just do a quick npm install to remove all of those packages that we're no longer using so that it doesn't spend any time on the build doing that. And I'm just going to head back over here to validate that our npm folder will be empty once which is finished. Just removing everything. And going with a framework-agnostic one means we have to install very little to make it work. So, here is our node modules. And it's basically empty. And so, now if I go to -- add that. I'm going to get commit and we'll say, back to basics. No? Push? All right. And then if we come up here, this is going to start a new build. And this will happen very fast because there's not really anything to do. This is gone that fail because I don't have a build script. If it ain't one thing, it's another thing. Okay. So, we'll change this to node build script. And... push one more time. I do have a Discord server. It's not mine, it's where I hang out. It's the party discord. It's a really good time. I would love it if you would like to come and hang out. It's a really supportive place for people who are content creators and who want to be content creators. Up and coming developers. People who have been doing it for years, people who have started, in boot camps and starting now. It's one of my favorite places on the Internet. So, with that, we have a site that's actually live. I go out here, go to my deploys. I'm going to go on here. We can sign up or log in, right? That's me. Okay. And then I'll do -- whoops. How about that? Here we go. All right. Confirmation message was sent to my email. So, let me pull my email up and verify this. Okay. I have signed up. I am logged in. I am now Jason. And if I run Netlify -- okay, find my site. Let's see. Let's close all these that we're not using. This one. Okay. login. Okay. We're logged in. There we go. So, now, what should have taken me 10 seconds if we had done it this way to begin with, we're a little behind schedule here. But we're gonna make this work. So, I have -- I have the ability login. So, next, we need to create a subscription, right?
THOR: Yeah. Next let's actually bring in our database. So --
JASON: Okay.
THOR: The database we're going to use to map to, A, keep a record of all of our users. But also, specifically to map our Netlify ID for the user to our customer ID in Stripe. So, yeah. It's basically a mapping table. And then later on I think we're also holding content in there. Yeah.
JASON: Yeah. So, I'm -- I'm on Fauna.com. I've created a new database. We're going to call it JAMstack subscriptions. And now we need to actually define it. So, we're gonna use the GraphQL method which is my favorite because it's nice and fast. So, let's create a database folder. I'm going to call this scheme. And in this of it, we need to define our types. So, we need -- for now, let's just start with our -- what users, right?
THOR: Yeah, I think we want -- a Netlify user ID and then a Stripe customer ID. I think that's kind of --
JASON: ID --
THOR: Basic --
JASON: And a Stripe ID. Right? And both of these are IDs. These should probably both be unique. Because we shouldn't --
THOR: Yeah.
JASON: Shouldn't be able to be the same Netlify user with multiple subscriptions. And shouldn't be able to be a Netlify user -- or reusing a Stripe ID. I believe we want those. So, we've now defined our user type. Right? And this is where I think Fauna's done a really good job is that they have set us up where if we go in here, go to JAMstack subscriptions, find this schema that I just wrote and I open it. It just created all the things that we need to create and delete users. So, if I do query, create a user. Oh, this is a mutation. Mutation. Create user. And the data that I want to send in. It's gonna autocomplete for me. So, I have a Netlify ID, we'll say one. And a Stripe ID, we'll say two. And then what's gonna come back is going to be a Netlify ID and a Stripe ID. And here we go. We've got this user. If I go out to my collections, I can see that I have that is created. That's not data that we want, so, I'm going to delete it. Now this is good. We're in, we've done it. We've actually created a full production database. Which is pretty amazing, right? I love how fast that happens. So, from here, we can do stuff with it. So, how do we want to hook this up now that we've got a database?
THOR: Yeah, so, I think we'll -- for Netlify Identity we have sort of listener functions or event functions, right? And so, basically, any time someone signs up, we want to trigger that function and we want to write a new record to our database.
JASON: Okay. So, that means that we can -- basically, we get like webhooks with Netlify Identity. So, whenever somebody signs up, we can trigger a function that will do something. So, I can write functions, identity, sign-up.js. And in here, we will exports.handler. And that will be an async function. And by default we want to return a status code of 200. And a body of something. So, this is -- this is a serverless function. We've just set up serverless functions on Netlify. That's all it takes. It's a functions folder, file. And then we told Netlify where to find 'em. And then you have to export a handler function. And you have to return a status code and a body. That's a serverless functions. And what I like about this, this is effectively a server. We've just created an API endpoint. What will happen is this is going to be available at our site/netlify/function/identity sign-up. And what's really powerful about this is that we can write as many of these as we want, and Netlify will deploy them for us. It's Node code. We can process images, we can talk to databases. We can use, you know, client -- client -- or like secret keys and stuff that you can't put into your frontend JavaScript. And that's why we're doing it here. Because we have to be able to talk to our Fauna database which requires a privilege token. We're going to have to talk to Stripe which requires privilege tokens. And so, serverless functions makes that possible for us. So, now that I've got this, what should I do? Like, what do you -- what's my next step here?
THOR: Yeah. Maybe as a first step -- so, I think from the event context we can get the user who signed up. And then we basically, just take the Netlify ID from that user and create a new database record. Maybe should we do that for now?
JASON: Yeah. So, here's what I'm gonna do so that we can see what comes back. I'm going to stringify the args and login. This is just going jut put this into the function logs. Now, one thing to keep in mind with -- let's just return okay so it doesn't explode on us. With webhooks, they have to be deployed. I have to add, get submit, feature, we'll say identity webhook. And then we'll push. Okay? And then if we go back to Netlify here. Under our deploys. We can see that it is running. And if we follow this down... And it will build nice and fast because we're not actually doing a build step. We're just reading from that HTML file. Okay. And it says one new function to upload, right if and our site is live. Now, if we go look at the functions tab, we can see that we have this identity sign-up function. And this is going to have a log. So, if I go out to site, out... just open the site. I think I closed that tab. Okay. So, I'm going to log out and then I'm gonna sign up again. And we'll say... and that will give me a confirmation message which I'm gonna go over here and click. There it is. Oh, damn it. Okay. I did this wrong. Because these -- these webhooks don't take the standard function's signature. That's on me. So, what we will see, though, if we look in the log, is here's the object of what was sent in. So, we get in our args, we get the path, we get the HTTP method, the headers. And then we also get the context. So, in the context, we get the client context, the identity, the URL, the token. And where is the actual user ID? Body. We've got the body. So, an event, sign-up, instance ID. And then we've got a user ID and we'll be able to get that out. So, this is what we're actually interested in. In the event itself, there's a body -- thank you very much for the subscriptions, y'all, I appreciate that. Bulletninja, Siya, appreciate you both. Now we're going to have event.body. And inside of event.body, we will have a user.ID. That's what we want to actually grab. The other thing we want to do is look at what the identity looks like. So, the payload looks like this. And we need to return just the user context. So, user metadata or app metadata. And that's the part that I screwed up. So, what we'll do is let's go rewrite this. And so, we're gonna get our -- oh, DOM you -- you gentle -- you gentle giant.
I have no idea what's happening.
JASON: I know. We just got so many subs gifted. I really appreciate it. Oh, Thor, now you're a subscriber. You can use the boop commands.
THOR: Oh! Okay.
JASON: Post some corgis. Troll yourself.
THOR: It sounds like I just outed myself as the biggest noop. Twitch noop.
JASON: It's okay. It took me a really long time to really understand what was going on Twitch.
THOR: Okay.
JASON: I need to return a JSON with new user metadata or app metadata. So, we need to return on stringify. And we need to add a role. And so, the role is basically, like we can specify these however we want. So, like sub-zero or three. Let's maybe just give 'em human names. Sub-free. So, we know it's a subscription and we know that it's free. Actually, let's do it like an actual role. It's a subscription and it's the free plan. That makes sense to my brain. And the thing we have to do up here, we have to get the event. And so, y'all are amazing today. Thank you so much. And we're getting buried in boops. It's wonderful. So, we now have the user. And the user is gonna come out of JSON parse event .body. This will give us our user. Let me actually log the user now. Okay? And in our user -- I put my phone on airplane mode and it's still making nose. Give me just a second. Shush, shush, you. It has a bunch of calendar invites like memorized. It's beeping at me. Just, you stop that. So, now -- what was I doing? Ah, yes. So, to recap. We got a handler. This is a special handler because it's an identity webhook. Instead of the standard response of status code and body, we're going to specifically return user metadata. So, either app-level metadata that can't be affected by the user. And/or user metadata that it be affected by the user. That would be where you keep like their name or some preferences that they could edit. We have the user object. And so, we're just gonna log that for now. And then we're gonna return app metadata that says they are a free subscriber. So, let's save that, go out here. And then --
THOR: There is a question. Where is the glue that the function identity sign-up is triggered? I think that's just automatically by Netlify -- our Netlify project, right?
JASON: Yeah. So, basically, the way that Netlify Identity works out here. Let me commit this and I'll show you why we do it. Let's see. Work in progress again. Logout, user info at role. Let's push. Okay. So, while we're waiting for that to go, Netlify Identity has three events that it triggers as webhooks. So, when you attempt to validate somebody who is signing up. So, if you wanted to do something like, say, only people with a valid @netlify.com email can sign up for this website, that would be this identity validate function. Then there's identity sign-up. After somebody has signed up through Netlify Identity, this will get triggered. And then there's login. Which is any time somebody does a login. And so, each of these is a webhook. When Identity performs an action, it will look for a function named one of these three things. So, when somebody tries to sign up, identity will look for identity validate. If it finds it, it will execute this function before it does the sign-up before after somebody signs up, Netlify will look for a function called identity sign-up. If it finds one, it will executes it. And the same with a login. If somebody logs in, it will look for this function and it will execute it. So, we don't have to set that up. That's automatically set up. We just have to -- all we have to do is go in and create these functions if we want to take advantage of this. So, that, I think, should have been enough time that we are deployed. So, let's go back and take a look just to be sure. Yep. We are deployed. That's -- all right. So, that's published. We can go over to our functions. Here's identity sign-up. Let's sign-up once again. This time I will be Jason 3. Okay. Wait for that confirmation email to show up. There it is.
THOR: It's really neat that you don't have to handle any passwords and stuff. That's such a rabbit hole.
JASON: Yeah. It's really nice to have. I'm wondering why my function didn't do what I wanted, though. Here's our actual user object. We've got an ID here. We've got -- here's our email. And we've got an app provider, user metadata. All of that good stuff. And I'm wondering if I... maybe it's... is it creating the identity at all? Let's see. So, it is creating it, but something is going wrong with the validation. hmm...
THOR: Is it the return value?
JASON: I thought I got that right. Let's find out. So, my return value. App_metadata: Roles: Admin.
AUDIENCE MEMBER: You're stringifying it.
JASON: Yeah. I could always sent it back as a literal string.
THOR: Does it need to be a string? Not on object?
JASON: Yeah.
THOR: Interesting.
JASON: Right? So, that's what's happening here. Metadata roles, admin. Can return a JSON object. Maybe it does expect an object? Let's try it. Maybe I'm wrong here.
THOR: I think it might expect also the 200, right?
JASON: Oh! Thor. It's early.
THOR: Yeah, sorry.
JASON: It is early, man. Why -- why am I even awake right now? I don't know how you do this, staying up late. My brain is like three train stops behind us. Okay. So, I was wrong. Every server less function has to return a status and a body. We need to update those docs. Those are confusing. Let's make this better. So, what are you doing? Get commit-am. Okay. we'll push that up. Let's try this one more time. Here's our site. Processing done, site is live. Okay. We're gonna try this. We've got here. Let's sync up the names with the numbering. Sign up. Okay. Confirmation message sent to my email. I went out here to get it. Come on.
THOR: Boop! How do we get the corgi stampede? Is this something I can do now?
JASON: You can, yeah. It's the JLang corgi emode. If you use enough of them, you will trigger a stampede. Ah-ha! That was it! So, not -- some chaos. Good. [ Laughter ]
THOR: Where's the music?
JASON: Y'all really went in on that.
THOR: Thank you. I needed that.
JASON: Holy buckets. Did that just work? Good, good, good. Now we're logged in. We can see that if we go back up to our functions, we have another object logged. Here is Jason 4. We've got an ID and everything there. And then if we go to identity, we can see that Jason 4 now has the role of sub-free. So, this is what we were actually after. Now we've got a free subscription. The part that we haven't addressed yet is how to link this up to Stripe. So, this is where I'm gonna lean on you a lot, Thor. So, where do we go from here?
THOR: Yeah. I think -- do we first want to create the database record in Fauna in that function and then we can add the customer object from Stripe? What do you think?
JASON: Say that one more time. You want to do the Fauna part first?
THOR: Yeah. So, basically, in that sign-up function, we need to create the record in Fauna. We need to create the Stripe customer object as well as sign them up to the free subscription. I was just wondering if we should just hook the Fauna stuff in and have that working. And then add the Stripe stuff.
JASON: Sure. Yeah. We can definitely do that. So, let's go over to this part. And so, the way that this is gonna work is we get the Fauna endpoint. And I'm going to go into my functions directory. And I'm just gonna Y -- and then we'll npm i-node_fetch. That's a tiny little utility that will let us use the web fetch in Node. And I installed it in functions because functions need to have access to their own dependencies. Because this functions folder basically gets zipped up and sent off to AWS. We need to make sure any dependencies they need are included in the functions folder itself. That also means that I need to, out here, in my Netlify.toml, I need to run Netlify install. I don't know who in chat taught me this, but this is magical. I'm going to install in the functions directory. Before I was changing directories and stuff. I don't need to do that anymore. So, thank you whoever in chat taught me that little trick. So, then, in here, we're probably going to want to extract this into a utility since we're going to use a few of these.
THOR: That's actually a good point, how do they learn npm?
JASON: I have no idea how people learn npm.
THOR: Are there actually docs for it?
JASON: There are docs. They're super-thorough. But because they're so thorough, they're super-daunting. People learn npm the way you learn a lot of CLI things. You learn it by tribal knowledge transfer. You hang out with somebody and they share a tip, that becomes your tip. And then you share it with somebody else. And eventually you get this network effect, everybody's got two or three things that power them up and slowly accumulate these pieces of knowledge. I don't know. There's -- yeah. I think that is definitely something that would be -- yeah. That's -- that's npm. Like npm won't do the install. So, if I'm out here in my -- if I do npm I, prefix functions. It's gonna install in the functions directory. So, it -- yeah. It's really -- it's a really powerful thing. Okay. So, in here, I need to do a call. Do const response = async fetch. We're going -- copied it. Dang it. I want this. And we're gonna send it as a post. And we want to send headers. So, our headers are going to be -- let's see. I've written this somewhere else. So, let me -- let me just pull up code that I use so that I don't have to do all of it again. So, we send to Fauna.com/GraphQL. We send in these cutters. I'm going to couple these straight across. This is an authorization header, we're going to use a bare token and then a key, I have to set up on Netlify real quick. And then in our body, we're going to send query end variables. Our body is JSON, stringify query. Which is going to be something. And then any variables that we need to send. And then in this case our variables will be the -- the user IDs. So, we'll have the -- the Netlify ID and the Stripe ID. So, we need to figure out what those would be. So, our Netlify ID is going to be user.id. And then our Stripe customer ID for now is just gonna be a number. So, it doesn't break. Then we're gonna take that query that we wrote. And this is something that I want to kind of take a second to point out. GraphQL doesn't require special libraries. You can just send it as requests. So, what I'm doing here is I am just writing out my query and then I'm gonna post it as a standard like fetch call. And I'm doing that because I don't want to have to set up a whole, like, GraphQL library to make this request. I want to be able to send it off and be done with it. So, get our Netlify ID in here, that's going to be an ID. And we need our Stripe ID, that's also going to be an ID. And we will set these to be the variables instead of our hard-coded values. Stripe ID. Okay. That is a GraphQL mutation in plain fetch. We've got this. And once we get done, say then -- and the JSON --
THOR: Wait, does that actually insert the variables?
JASON: Yeah. We declared our variables here, using them here and passing --
THOR: Do we need the curly brackets?
JASON: Do we need the curly brackets? This is GraphQL, this isn't JavaScript.
THOR: Gotcha.
JASON: I can show you how this works in here. So, let me take this function out. Let me put this back in here. And then we can move this over. To make this -- and down here we can set our query variables. And we'll do Netlify ID. And set it to 2. Stripe ID, and set it to 3. And then up here, so, we've got a query variable and then we use the GraphQL variable here. So, when I run it, we get 2 and 3. Basically, what we're providing is this is the query and this is the variables.
THOR: Gotcha.
JASON: So, we're just plugging those pieces together in there. Query and variables.
THOR: Nice.
JASON: We need to make our response, make sure we get it back at JSON. And it if anything goes wrong, we just want to log that so that we can see what went wrong. Did opportunity like fetch, why? Oh, wait. Not async. Somebody in chat already tell me that? Okay. So, we're gonna await Fetch. Then we get a response. And in our response, we will console.log response. Just to see what we get. And let's make sure that that's clear that it's the response. Okay. We feeling good about this? Are we happy?
THOR: I think so.
JASON: Okay. Let's... with add... okay. Good. Get commit. Store. Netlify user ID in Fauna. Okay. So, we'll push. All right. So, now we have our site deploying. Big deal. Look at the chat. What typos? What did I do? I didn't do nothing. Yeah, see, we will need to do some checks to make sure everything comes back all right. For now, we're just kind of -- we're just gonna ship it. So, let's see, we've got this up. Let's go into functions. Pull this up again. And let's add another user. Okay. Get over to my email. Grab this. Run it. Ship it. Good. Okay. So, now, assuming everything went well, it didn't work. And we're gonna find out why by checking our logs. And our logs say: Response errors object. Oh. I didn't set a key! I didn't add my environment variables. Of course it won't write because we didn't give it permissions. So, for this, I'm going to go into the security area and I'm gonna get an API key. So, I'm gonna pull this off-screen so that you hackers don't steal my stuff. Oh, actually, let me show you how this works. So, we're in this screen here. So, you're gonna go to security. You're gonna hit new key, and we want a server key. I'm going to give it a name. We'll call this like, JAMstack-subapp. I don't know. When I hit save, it's going to show me my key. Let me pull that off. So, I have saved that. And then once -- once that is saved, it lives here. And you can't see it again. You can just revoke it. So, if you need a new one, you hit new keys. And then I'm gonna go into here. We're gonna go to settings. Build and deploy and environment. And then I want to add an environment variable. I need to remember what that environment variable is called on a server key. So, I'm gonna create Fauna server key. And I'm gonna paste my key here. And now I'm going to navigate away from this page. So, I'm gonna back to the functions page. All right. So, now my key is here. You hackers get nothing.
You hackers! You dirty hackers!
JASON: Now I've run this. And doesn't help us in this case, we're not running anything. New Netlify is bringing in the environment variable. We can work with the local functions with the environment variable. It doesn't work with API, it needs servers running in the background. But that's a really nice thing to have. So, now that we have an environment variable, I'm going to make one quick change so that we can actually read the error. Let's do it this way. And we'll have a JSON stringify, if we get an error, we'll know what it says instead of just getting an object. Let's commit this add key plus stringify errors. Push. Whoof! This is a lot of app to build. I am -- I am feeling the pressure of, like, holy crap, we got to get through a lot.
THOR: Yeah, yeah. We haven't even gotten to the stripe part yet.
JASON: The nice thing about in is most of what we're doing is we're clearing boulders that make the rest of it easier, right? So, we've got this like identity and authentication. And if you think about -- I don't know how many of you in the chat have built authentication into apps before. But like the fact that we have been able to do it in an hour is, like...
THOR: Yeah.
JASON: Me 5 years ago is staggered by that because it's such a huge leap forward in terms of the barriers to entry. And the fact that we have not only been able to set up Identity, but set up Identity and a database. And assuming this fix works, be able to have our Identity instance update a third-party database. Let's find out if this works. Show me potato salad! Okay. It didn't fail. That's a good sign. Let's go look at our function log. Actually, that's not what I want to do. I want to look at --
It would be neat if this just worked.
JASON: Collections. Look at that. Look at that work. So, now we're storing user IDs automatically on account creation. So, I feel like this was the hard part. Like getting this part wired up, unbelievable.
Holy buckets --
JASON: Yeah, holy buckets indeed. So, next, we have two major hurdles left to clear. The first one is, we need to get into Stripe in general and set up a subscription. The second hurdle, once we get that, we need people to be able to manage their subscription and go from a free to a paid tier. And the third one after that, the app needs to know which tier we're in and appropriately serve data.
THOR: Yeah.
JASON: So, how far can we go?
THOR:I think we'll make it.
JASON: Switch into Stripe.
THOR: Create a fresh testing account for this. You can log in with your credentials. But you can simply click "Create a new account."
JASON: Okay. Let's go here. And then I'm gonna go up here and create a new account. We'll call this JAMstack -- or, no. We're gonna call this corgi subscriptions.
THOR: Imagine --
JASON: So many things.
THOR: Just get a corgi subscription for a month.
JASON: Right now, especially being in quarantine, if I could get a dog in my house for one hour a day that I could just like pet. Ah, I would do it. I would do it in a heart beat. Okay. So, I have -- I'm here. I'm in. I've got a test account. I'm ready.
THOR: Notice. So, yeah, maybe -- should we actually set up the Stripe key in -- in your Netlify environment variables to get that out of the way?
JASON: Let's do it. So, these are test keys, so, I don't mind sharing them because they're not going to -- there's no risk here. So, I'm gonna have my publishable key -- I need both of these, right?
THOR: No. Currently -- well, let's set both up. But, yeah.
JASON: Okay. So --
THOR: I don't think you need them.
JASON: I'm doing this off-screen because I don't want to show the other tokens. But I'm going my environment variables again and I'm just gonna add a Stripe publishable key. And then I'm going to add a Stripe secret key. And I need to get that value. So, let's go here. Grab it. Drop it in. Saving. Okay. So, now I have in my -- in my set of my environment variables, I have my Fauna server key and Stripe publishable and test key. These are test keys, you can't do anything with them other than like test transactions. But this will give us what we need to get going. So, let me bring this back over. All right? And now I'm -- I'm ready. When we go to Netlify Dev here, we can see that we now have the Stripe publishable and Stripe secret keys. That's what's ready to set up. We are ready to rock and roll.
THOR: Fantastic. So, the next step, we kind of need to model our subscription business model.
JASON: Okay.
THOR: So, basically, we need to create a bunch of products in the Stripe dashboard.
JASON: Okay.
THOR: And basically, what we'll do, we'll create a product for kind of every tier. So, we'll -- we want one content-free product. Or -- yeah. If we just call it "Free." And then --
JASON: These are optional. I don't need those, right?
THOR: No. No. Just leave them.
JASON: Okay.
THOR: Plank. And so, this is our product. And every product can have multiple prices. So, for the free price, we're just gonna set zero US dollars. And I mean, what this allows us to do is to basically track who has signed up and who is currently running on a free plan. And then when we track that in Stripe, we can actually ask someone upgrades or downgrades, we can track that in the reports from Stripe.
JASON: Nice.
THOR: That's why we're creating the free plan here. Basically, just leave it like this.
JASON: Okay.
THOR: Yeah. Zero dollars monthly.
JASON: Yeah. This is cool too because metered usage is really interesting. That's probably a whole other thing we can talk about later. I really like this because it puts us in a -- like a very exciting space where like we can say, all right, you pay me a plat rate per month. Or you pay me X cents per usage of a thing that I have. So, our free plan. Charge in US dollars. Does this pricing plan have multiple price tiers based on quantity. Oh! Is that like if you buy one, it's $10, if you buy 10, it's $85 or something?
THOR: Yeah. Exactly. You can get pretty complex here. Yeah. You can build pretty complex business models. But sometimes -- and the easy business model might actually be nice for your customers also to understand what they will pay. But, yeah, it can get pretty complex.
JASON: So, do I want a new product or do I want a new pricing plan?
THOR: So, now we have a new product.
JASON: Okay.
THOR: The free product is just gonna have one pricing plan. But then let's maybe call this basic. And then have another one, pro. So, maybe this is the basic plan. And that gives you some, maybe, basic content or --
JASON: Okay.
THOR: And --
JASON: Product, we'll call it basic again. It's reoccurring.
THOR: And so, now let's actually call -- sorry. Let's call the plan. Let's get more specific here. So, we're gonna call it basic monthly US dollar. So, maybe do basic, hyphen -- yeah. Monthly USD.
JASON: Okay. So, I've got it. We'll say it's $10, billed monthly. Customers subscribe to one unit will be billed $10 monthly at the beginning of each period. Wow, that is easy. Okay. Here we go. Add pricing pool. We're gowning to do one more.
THOR: Yes.
JASON: Now we're gonna add --
THOR: So, actually for the basic plan, let's add a yearly plan also. But we can -- yeah. So, and basically, if you subscribe to the yearly plan, you get a little discount. So, we can do basic yearly USD. And as you can see, you could also have different currencies here. So, if you wanted to offer Euro, you know, for your European customers, you could model all of that as like these different prices underneath your products.
JASON: Got it. Okay. So, the plan will be, if you go monthly, you pay $10 a month. If you go yearly, you get two months free, it will be $100. So what about it seems good to me. Let's add that. All right. And that's cool. So, we've got two plans. We can see them here. Very exciting. Now I'm going to go into products. Create new. We're going to go premium. All the best corgi content. And then we'll create this. And say premium-monthly USD. This one's we're gonna say 50. And same deal. Good, good, good. Right? Then we're gonna add a new pricing plan. We'll say premium-yearly USD. And this one's gonna be yearly. And let's see, we'll call it 500. Same discount. You get two months free if you go annual. So, add a pricing plan. All right. So, we have a monthly and yearly basic and premium plan. Come out and look, two pricing plans, free plan. All right. These exist. Now what do we do?
THOR: Awesome. Now we're in the Stripe dashboard, we have a new product that we recently brought out which is this self-serve portal for subscriptions. And especially for our case here where we sort of have a limited amount of time to get this live, we can use the self-serve portal to allow customers to make modifications to their payment methods. To upgrade, downgrade, all of that good stuff. And so, we can actually start off with configuring the portal features and UI. Maybe if you just want to click on the first step there.
JASON: Okay. Portal settings. So, I just jump over here?
THOR: Uh-huh.
JASON: I'm not gonna change the branding settings or the account information. Yet, I want you to see your billing history, yes, I want you to be able to update your payment methods. Yes, I want you to be able to update your subscriptions. prorating seems reasonable. Do we want cancellation? Or just want people to go to the free plan?
THOR: No, we can allow them to cancel if we wanted to. Just -- just to kind of show that it's a different webhook that we have to handle. So, maybe, yeah. Let's allow cancel also.
JASON: Okay. Credit -- okay. All right. Then we need to add all of these, right?
THOR: Yeah. So, we want all of our products and plans, pricing plans.
JASON: Okay. There's our -- there's our setup. Business information, looks good. This is required. So, I'm just going to set this to be our home page.
THOR: Yeah.
JASON: Because we don't have those, but we need those. Our default redirect link after they manage their account to probably just be the home page. So, let's just save all that, right? Anything I'm missing?
THOR: No, I think that sound right. You could actually click on the preview button which would show you what kind of the portal will look like to your customer.
JASON: This is cool. And I change --
THOR: And, yeah. This is especially when like you're getting started or, yeah. It's a lot of the functionality. Like updating your payment method, upgrading, downgrading. Which if you built this custom, you, of course, have a lot more control. But also it is a good bit more work.
JASON: This is very cool. Okay. So, yeah, we're all set here. That's good. So, I'm gonna save.
THOR: Yeah.
JASON: And then we are -- we're good to go. So, let's go back to the instructions. I set my product catalog. We did that part. Set the limitations, previewing, okay. So, now we need to implement a redirect. Good deal.
THOR: Yes. So, we'll get to that part. The first thing that we actually need to do is we need to sign-up. We need to create a customer option for them. And then we need to sign them up to the free plan. And we can do that in our sign-up function. So, where we have the place holder for the Stripe customer ID. Let's use the Stripe Node library. So, we would have to npm install Stripe.
JASON: Try this. Npm install Stripe, prefix. I've never tried this one before. It might not work. Functions.
THOR: Let's see.
JASON: And that's the library, right?
THOR: Yeah. Just Stripe. That is our Stripe Node library, yes.
JASON: Does it install during functions? Take a look. Ah! That's cool! I love -- I love --
THOR: It's really cool.
JASON: They make life so much easier. We have got that now. I can bring it in. So, Stripe equals require Stripe. Okay.
THOR: And we'll need our server key, secret key passed in there.
JASON: Okay. And now for those -- so -- wait, remind me. Is it like new Stripe?
THOR: No, I think it's require Stripe. And then we just -- we just call it.
JASON: Like right here?
THOR: We just chain it, I think?
JASON: Okay.
THOR: I do this so often. And I still don't know. Maybe check the docs.
JASON: Look at the docs? Let's go look at the docs. All right. Npmjs.com package, Stripe.
THOR: You can look in the docs for the billing portal as well. Every Stripe request.
JASON: It's there.
THOR: Oh, okay.
JASON: So, not an object. It's just the key. So, process.end, Stripe secret key. Validate that's what I meant. Stripe secret key. Now we have a Stripe object. Now down here, I can do -- what do I want to do to create a customer?
THOR: Yeah. So, you basically say comms customer equals awaits.stripe.customer create.
JASON: Is that right?
THOR: Sorry. A little bit of a lag on my customers -- yeah. It's always plural.
JASON: Is that right?
THOR: Yeah. Customers create. And then we just pass in an object. And let's just set the -- do we have the email address?
JASON: Let's check the logs! Let's go to the logs, Dan! Um, here we go. We do, indeed, have an email.
THOR: Nice. So, let's just say email.
JASON: User.email. Okay. Good. Anything else?
THOR: No. That will give us the customer.
JASON: And then can I just do customer.ID?
THOR: Yep. That is correct. But before that, let's actually subscribe them to our free plan just to make sure we have them signed up.
JASON: Okay.
THOR: So, between the customer and the Stripe ID, just do await Stripe.subscriptions.create.
JASON: Subscriptions.create.
THOR: Correct. And we'll need to pass in the customer. Let's actually --
JASON: Like --
THOR: Maybe go to the API reference. That might be easy.
JASON: Yep. Good call. So, let's go Stripe, sdk, is that going to show up for me?
THOR: If you just go Stripe API ref.
JASON: Stripe API ref. Sorry, what's the URL?
THOR: It's Stripe.com/docs/API.
JASON: API. Okay. And then we needed to create a subscription.
THOR: Yep.
JASON: Subscriptions. Am I -- create a subscription. Go in with Node. Stripe.subscriptions.create. And then pass in a customer ID and items. Okay. Let's just copy this across. Okay. And in here, we need customer.id. And our free plan is going to be in here somewhere.
THOR: The dashboard, yeah.
JASON: This one?
THOR: Yeah.
JASON: Or is it the plan?
THOR: Under the prices. So, if you click on the free prices and then free. If you click on that, and then in the top right corner there is a copy. You just click on it.
JASON: Okay. Wait. Did it already know my plan name?
THOR: Yeah. Because you're logged into your dashboard, it actually uses Real IDs from your account.
JASON: Listen, Stripe. Y'all got to stop being so smart. Okay. So, let's -- we don't need that user ID anymore. Should I log any of this for, like, no. We're okay.
THOR: We can look in the dashboard log. So, basically just, yeah, Stripe ID, customer ID, that should be it pretty much.
JASON: I guess -- let's ship it, right?
THOR: Yeah. Show me potato salad.
JASON: All right. Here we go. Create Stripe.customer/sub. Okay. Push. All right. And now we go back out here.
No, this will work, this will work. Yes.
JASON: I'm feeling -- I'm feeling pretty excited right now, y'all. This is really -- this is really cool. So, we have -- I hope, I hope, fingers crossed -- this is gonna work. Okay. We're live. So, I'm gonna come back out here to here. Let's change this to number 7. We're gonna sign up. Go to email. Go in here. Let's verify that account. Okay. No errors. All right. Moment of truth. Oh! We have done it! That is amazing. Of okay. So, now we have the ability to, like, get this information, right? So, maybe -- maybe what we can do -- wait. Holy buckets did that work? Oh, that's too bad. I mean, that should work. What a strategy that that's not there. Okay. So, anyways, the -- so, we've got that. So, what do you want -- how do you want to do -- wait --
THOR: Let's quickly check the Stripe dashboard logs just to see that the customer is correctly signed up for the right subscription.
JASON: New customers. Do I go here?
THOR: Yeah, I mean, you can go to customers on the left.
JASON: Customers on the left.
THOR: Left menu, yep.
JASON: Subscribed to free.
THOR: Nice. Okay. So, that actually did exactly what we wanted.
JASON: Magic.
THOR: As a next step, to show when the customer is logged in to actually show them their account details, maybe.
JASON: Show account details. So, you want to -- you want to --
THOR: Which is basically pretty much right now we just have their subscription tier, I guess.
JASON: Right. We want to do something like, let's say, get -- get the user details. That seems like --
THOR: I think we actually have the role in the JSON token, right?
JASON: We do.
THOR: So, if we just log that if they're logged in.
JASON: You're totally right. We don't need that one. So, get out of here. All right. So, let's go here. And then we're going to add some JavaScript. And the JavaScript that we're going to add needs to get at the -- at the user details. So, we're going to use Netlify Identity. And I've got some notes down here so that we don't burn a bunch of time trying to make this work. I did some -- is this the one? I put notes together and they're bad notes. Just not great notes. Okay. So, I'm gonna go in and look at my repos. I created a private repo that does some of this. I'll open source it once we get through it all today. And in here somewhere, there is a... Netlify Identity webhooks. Get that one out. And we figured out on the front page that we needed to have -- ah. Here we go. We get the window.NetlifyIdentity. And that's going to get us user. Start there. We will get our user and we will -- let's add like a dumping ground here. So, what we'll do is we'll say document.querySelecter, pre. And then we'll just set the inner text to be JSON stringify, user null two. What are you trying to do? Get out of here. Okay. That's our basic setup. I'm going to run Netlify Dev. And now out here on our local host. I'm able to show what we've got. So, we've got our app metadata. I need to refresh the token. So, let's make sure that we refresh that token. Here. So, before we do anything, we're gonna refresh our user token. And then we're gonna do stuff. And the reason for that is that we're gonna be changing the roles. And we always want to make sure that we have the latest information. So, we're just gonna send off a quick request. And so, what's happening here is Netlify Identity is going to get our current user and then we're gonna get a JSON web token and this argument is to force a refresh. The result that comes back is a JSON web token. So, let's clean this up a bit and we'll say that const token = await. I think that will work. And then this will let us -- here we can just console.log -- no. We're not gonna do that. We're going to console.log the token instead. And let's make sure that works. I really wish you would stop talking. I think that is already refreshed. So, let's go back to our page here. Let's reload. And it didn't like that. Uncaught, in promise. index.js132. Not 100% sure that any of that does what we want. So, instead, let's just do a dot then, and we'll get our token. And we'll do this -- we'll do this dance out here so that I'm not burning a bunch of time trying to figure that out. Could not read property of -- T of null. Oh, okay. So, let's figure out what the user is, I guess? Hm. Might have lost my user here. So, let's... JSON.stringify, user, null 2. We don't have a user. So, basically, I think we need to say, like, if no user, we can just return.
THOR: Okay.
JASON: Okay. So, now I can log in. I'm going to log in as -- what was my username out here?
THOR: We're at Jason 7?
JASON: We're at 7. Okay. So, I'm logged in.
THOR: Okay.
JASON: And now we get an access token. Okay? That's what we wanted. So, let me take my access token over to here. And we can see we've got our roles. Perfect. That's what we wanted.
THOR: Nice.
JASON: So, that is very exciting.
THOR: How do we get the role out of there?
JASON: We can do it by -- actually, we're just gonna get this. So, I think what we can do is we can actually get, like, we'll just get the user again. So, we'll say window -- I think I can just do window.netlifyIdentity.currentUser. And then let's log the -- let's log that instead. CurrentUser.
THOR: Okay.
JASON: There we go. And we can see the roles are here. Perfect. So, that's gonna be user.app metadata.roles. So, if I want to get -- we'll say roles = currentUser.app_metadata. Good. Here. All right. And then we can say something like -- we'll just log on to roles and see that it's giving us what we want. Good. There's our roles. Now we have a list of roles to do whatever we want with. I think in the interest of time we can just stay there.
THOR: Yep.
JASON: All right.
THOR: So, let's just add a button now and have the button just say "Manage your subscription." Or, maybe subscription portal or something like that.
JASON: Okay. I'm wondering -- I think I forgot -- I don't know why this is doubling up text. Looking at -- I probably forgot to include something. But let's just not stress about it. And instead, we're going to add a... I'm kinda -- we're skipping a step here. Which is that I would want -- I would want this to be only displayed if we were logged in. In the interest of not, like, going down a rabbit hole here, I'm just gonna have the button work. And so, we'll say -- oops. We'll give it a name of managed sub and then we'll make sure that we just get the check.
THOR: We're getting tons of questions. Yes, so, basically, the -- the roles in Netlify directly map to our plans. So, sub-free, sub-basic, sub-premium, these are the roles that we're going to set on our Netlify Identity user. And basically what we're going to do in the next hour is hook up the Stripe webhooks when someone changes their subscription to then find our user mapping in Fauna to then update the role in Netlify Identity. Does that make sense? But the first thing we need to do is we actually need to be able to send them to the subscription portal. And so, we're gonna need a new Netlify function that just reads out the customer that is logged in. From the database finds the correct Stripe customer ID and then creates a portal login link.
JASON: Okay. So, and like -- specifically there are a bunch of ways that we can solve this problem, right? We could have -- we could basically pull all of our information out of the database and any time you updated your plan, we could like update the database from Stripe and then every time that we requested content, we could hit the database to make sure you have access. There are so many ways to do this. We're assigning the JSON web token roles. That's across everything. We can immediately bail on a function if you don't have the appropriate role to access it. It lets us shortcut a couple things. But ultimately, you can solve this a lot of different ways. Don't take this as the only solution. This is just a solution. So, you said we need a function to --
THOR: Create the redirect link for the portal.
JASON: Okay. So, we're gonna create a managed link. And that, I assume, is gonna need Stripe which is require Stripe. And that needs our process -- whoops. process.env.stripe_secret_key. And then we're gonna export handler and async function that will return a status code and a body. Okay. So, that's the guts of it. And then we're gonna have an event and we're gonna get context. And the nice thing about this is that our context, if I can spell it, is going to include the user object.
THOR: Nice.
JASON: And so, that user object will have -- we'll get user out of context.clientContext. And this includes basically whatever's in the JSON web token. And if our case, if you're logged in, you're going to get a user. Then we can take that user and we can do something like, let's write our query. I want to query by ID. And I think that might mean that we need to query -- find user by ID. Which ID are they gonna let us search by? That's gonna be a different ID. We need to search by Netlify ID. So, I need to write a new piece of our schema. And we're going to get user by NetlifyID. And that is going to take a NetlifyID, and it's going to return a user.
THOR: And we should also add the one for the Stripe. Because later on we will get a webhook from Stripe and we need to be able to find the user by their Stripe ID. So, let's add that query also.
JASON: GetUserByStripeID. And that will also return a user. Okay.
THOR: Yeah.
JASON: Now that we have that, I'm going to go to Fauna. I'm going to update the schema. And we'll do that by just grabbing that schema that we already have. And I think that worked. Good. It's going here. Now we have a GetUserByStripeID and a GetUserByNetlifyID. We can do something like get user by Netlify ID. Grab one and check. Here is our user. Get --
THOR: That's the Netlify ID.
JASON: That's the wrong one. Netlify ID. Use the Netlify ID and pass this in. And we want to get back the Stripe ID and the Netlify ID for confirmation. We're not using those. So, we can collapse them. Go away. And this gives us our customer I did (and our Stripe ID. Cool. So, we can take this, get back out here. We will create manage links and we're going to start by writing our query that is going to be here. All right. And in this case, we need a Netlify ID and that's going to be an ID. And we're going to drop that in here. And then we also need variables. And those variables are going to be Netlify ID. And that will be user.id. Okay. So, then what I want to do because I don't want to rewrite this code every time that we use it, is I'm gonna take this and I'm gonna create a little utility function out of it. So, let's create utils. We'll call it Fauna. And then I need to get fetch. Node-fetch. All right. And this is going to be async function. Just call it FaunaFetch. Yeah. Why not? And that is going to accept query and variables. And it's going to return. And we will just pass in the query. And the variables. Okay. So, that's gonna do all the work for us. And I am -- oh. Let's see. Exports.faunaFetch is going to equal an async arrow function. because I just want this to be a named export. Let's do it like that. And then in this function, I will const faunaFetch equals require utils/fauna. And then I can just -- we'll say result equals await, faunaFetch, query variables. Okay. We built a little utility so we can very quickly send these off and not have to over think them. And then once we get that back, we want to create a management link. So, our result is going to include -- let's just console log that. No, let's just for now -- we can just return it. This is my favorite way to debug these. We can just JSON.stringify the result. And then out here we can create a function. And this function will just use the Fetch API. So, we'll say, Fetch. We'll send it to .netlify/functions/get-manage-link. And we're gonna use method: POST. We will send in headers of authentication. And then we're gonna send in a bearer token that is our access token. I'm just gonna drop that right in.
THOR: Nice.
JASON: And then I want to send in -- I don't know how --
THOR: That should be it, right?
JASON: I don't need anything else. That's all I need. I'm going to get .then. Get the response. The response is going to be JSON. And then we'll catch any errors. We'll just console.error those. Okay. And assuming that -- oh. And then because we're not catching it yet, we'll just log that response.
THOR: Will we need to deploy the function for it to work?
JASON: Nope.
THOR: Nice.
JASON: So, now that we're not using webhooks, let me just start Netlify Dev over again because I can't remember if the function existed. Now that we are at local host, we can come out here. Let's refresh. It doesn't like that. So, something didn't work. We're getting a 404. Netlify functions -- Netlify functions/get-manage-link, did I mess this up?
THOR: Index.HTML?
JASON: Create manage-link. Okay. So, we got errors. Variable NetlifyID expected value of type ID. But value is undefined. So, I sent in the wrong data. NetlifyID. NetlifyID. By variable. User.ID. Oh, wait. This is -- what is it? It has a different value. Let's go check the logs real quick. It has a name that's not ID that is kind of confusing. But we can still use it. So, if I look at functions, look at this identity, oh, no. Let's just log. We'll just log things. Console.log. User. Okay. Let's try that again. And we'll see this in the console that we'll get a sub, not a -- not an ID. So, I just need to change this over to use user.sub instead of Netlify -- or instead of ID. And now if I reload, we get data. Get user by NetlifyID. That's good. That means we've got what we need. We don't need to return this. Instead, we know that we're going to get back a result and our Stripe ID, then is result.data.getUserBy -- I really wish I could make that stop -- NetlifyID.stripeID.
THOR: Awesome.
JASON: That's doing what we want.
THOR: If you go back to the documentation to the portal, you can copy the request to create the portal link.
JASON: Okay.
THOR: And you can await that.
JASON: Await. Link equals await. And the customer is going to be our StripeID. The return URL is going to be... is that gonna work for me? I think that'll work for me? And then --
THOR: Asking why user dot -- what was the reason?
JASON: If I'm being completely honest, I don't know. I assume this is a holdout from a previous iteration where everyone who had an identity account is a subscriber to like you're a particular go through API or something. In all honesty, I think it's just a case where something was named and never got renamed when that name started to mean something else. But, yeah. So, it's -- it's -- that's your Netlify Identity sub-ID. You've created an account. You're subscribed to the account. Naming is hard. I think this works. I might have to hard code. Let's give that a shot. But this --
THOR: No, that should be cool. Yeah. And then --
JASON: Let's get back what we get.
THOR: Yeah, awesome.
JASON: All right. So, let's give this a shot. Here is our ID. We've got a billing portal session and our URL. That is dope.
THOR: So, we'll just redirect over to that.
JASON: We'll do this at one time. Let's go into -- oh, you just want a repo, shoot. That is cool.
THOR: Okay.
JASON: We can basically make it so this will all execute and then redirect. And I have done this before. Where have I done it before? I've done it on my own website. So, let's go to my website. And we'll look at functions. Which are where?
THOR: Where are they?
JASON: Functions. Subscribe. After you subscribe, that's right. So, you send the status code of 30 1. You send headers, the location you want to send to is the page you want to go to. And then you give a body. So, I'm going to say, redirecting. And we'll set headers. Location. And the location that we wanted to use --
THOR: Link.URL.
JASON: Link.URL. This is going to happen immediately, which is not what we want. We're going to wrap this up in a function. But let's test that this actually works. No!
THOR: I think but we'll have to.
JASON: Oh. That triggers CORS which is probably a good thing, I suppose. Actually, you know what? Let's see what happens if we do it like this instead. So --
THOR: Yeah, we could redirect on the client.
JASON: Yeah, let's redirect on the client. We're going to get our manage link. And instead of sending this back, we're going to send back our link.URL. Okay. Now what we'll get back is our URL. No.
THOR: Yeah. I mean, you could just send the whole link object. But, yeah, that works, cool. So
JASON: Now there's our URL. Now that we've got that, I'm going to wrap this up in our function. And our function is going to be called redirect to manage. We will do all that inside of a function. What we can do in here, we can -- window.location --
THOR: Should we do push?
JASON: Isn't it just like equals?
THOR: Yeah. I just -- I think if you use the dot push function, it gets submitted to the history or something? What -- I remember this.
JASON: Yeah, yeah.
THOR: This feature or something.
JASON: Yes -- JS navigation? Nope. Navigator.
THOR: It's just location push, no?
JASON: I don't think it was location push. It was -- who remembers, chat? Somebody knows this.
THOR: History. Someone says history push?
JASON: History. History. Okay. So, show me how to push state.
THOR: Location should --
JASON: That seems like a lot. If I just -- MDN, window, location. If I just want to move somebody, assign, reload, replace. Location Href. Okay.
THOR: Yeah, I mean, let's --
JASON: Yeah. Maybe let's not bog down here. I'm going to do something that might not be the right idea. But we'll do window.location.href equals response. We'll just name that. And I think that'll just work. So, let's --
THOR: Fingers crossed.
JASON: Redirect manage here. Okay. And so, then what we need to do is we need to say docu -- to put it below. So, we'll say underneath this we're going to say document.query selector. And it was manage? Manage sub. This pop-up is going to kill me. We're going to add event listener and we will say click and we will run redirect to manage. So, now this should happen when we click the button. So, let's try it. Okay. First, nothing happens. Then I click this. Then it redirects. Okay. So, now --
THOR: Awesome.
JASON: If guy into change my plan, I'll select. Confirm. And it wants my info. I'll put in the Stripe test card which is any all 4242, and then put in any future expiration date and number jumble for the rest. And now I have changed my plan. So, this is exciting.
THOR: Awesome.
JASON: However, if we go and look at the identity, we can see that I'm still sub-free. That's no good. We don't want that. What do we do now to make sure we're updating our subscribers appropriately?
THOR: Going back to the portal documentation, there's a section on webhooks. And so, one of the webhook events that we want to handle is for upgrading and downgrading subscriptions. And so, specifically, it is the customer.subscription.updated event. You can actually go to your Stripe dashboard and see the update for what you just did. So, if you go to developers, events.
JASON: Events.
THOR: Yep. And you will see that there. So, that's the customer subscription updated. And then I think we have items -- items.data.plan. Yeah. And then we can match on the nickname or the plan ID if we had the plans in the database. But I think for now we can just use the nickname.
JASON: Yeah. I think that will be okay. Cool. Good. so...
THOR: So, we will need another function. And maybe we just call it handle webhooks.
JASON: Handle webhooks. Okay. And this one I assume we're gonna need Stripe.
THOR: Correct. Yes. I think we can actually -- we have like a webhook handler, Netlify function in one of our existing projects if we go to the Stripe sample. So, maybe we can just copy because we do like the -- the webhooks signature verification stuff.
JASON: Okay. So, I want to go to GitHub.com/stripe-samples. And which one am I looking for?
THOR: I think if you just type in Netlify in the search. There it is.
JASON: All right. And it was handle-purchase.
THOR: Right. The hands-purchase. Exactly. And so, we should just --
JASON: We want this. There's a webhook secret that we don't have. We're going to need to create that.
THOR: Correct.
JASON: All right. So, to walk through this code, what we're doing is basically we get back, in the event we get body and headers. We are telling Stripe webhooks to check that the event is legit. And we do that with a webhook secret. That prevents someone from spoofing. Like saying, like, I just bought all your stuff and they could send an event to this function --
THOR: Give me access.
JASON: Right. So, the webhook secret validates that it's legit. It was an actual Stripe event that was actually paid for and all these things. That's why we have to do this step. Otherwise I could just learn the function names and that would do it. For my users stuff, that's everything that I use. If you are curious about the tools and stuff, VSCode themes and fonts and all that good stuff.
THOR: It's really cool. I basically just copied your VSCode theme.
JASON: Okay. I need to get this secret. Let's go to the dashboard. I'm going to go to webhooks. How do I create my secret?
THOR: Well, so, actually do you have the Stripe seal I installed? Because we can forward webhooks to our local host.
JASON: Yes, I do.
THOR: Nice. So, let's actually do that. If you type Stripe login just to make sure we are connecting.
JASON: Enter to open the browser.
THOR: Yep. To the correct Stripe account.
JASON: Corgi subscriptions.
THOR: Yep. So, allow access.
JASON: That's cool. I like that. I'm allowing it. I may now close this window.
THOR: Nice. I'm logged in. If you just run -- we will give you the webhook that was used for local testing.
JASON: So, I'm gonna --
THOR: Set that up. So, yeah, let me pull this offscreen real quick. Add this in my environment. Environment. And it was Stripe webhook secret, which I'll set to the value that the CLI just output for us. And save that. And get out of this screen. And we'll come back over here. All right. So, now I have a Stripe webhooks secret. And if I run Netlify Dev again, it is now available. Good. So, I can go into here. And or, actually, let's go back to the samples and I'm just gone from copy this, I think, I'm gonna copy this wholesale.
THOR: Yeah. And then we can hollow out the if statement. [captioning disconnecting in one minute [
JASON: So, what was that?
THOR: Come on, GitHub.
JASON: I think that was -- that was bad copy-paste on my part. So, we are not going to send any mail with this one.
THOR: Yeah, we can actually delete most of the, yeah.
JASON: Okay. This is the whole part we're to the gonna need. [captioner disconnecting]
Learn With Jason is made possible by our sponsors: