You have unlimited access as a PRO member
You are receiving a free preview of 3 lessons
Your free preview as expired - please upgrade to PRO
Contents
Recent Posts
- Object Oriented Programming With TypeScript
- Angular Elements Advanced Techniques
- TypeScript - the Basics
- The Real State of JavaScript 2018
- Cloud Scheduler for Firebase Functions
- Testing Firestore Security Rules With the Emulator
- How to Use Git and Github
- Infinite Virtual Scroll With the Angular CDK
- Build a Group Chat With Firestore
- Async Await Pro Tips
Metered Stripe Subscription Billing
Episode 121 written by Jeff DelaneyHealth Check: This lesson was last reviewed on and tested with these packages:
- Angular v6
- AngularFire2 v5.0.0-rc.11
- firebase-functions v1
Find an issue? Let's fix it
Source code for Metered Stripe Subscription Billing on Github
Update! Watch the latest video and get the most up-to-date code by enrolling in the Stripe Payments Master Course on Fireship.io
Today we will something very ambitious - our own Software-as-a-Service (SaaS) product that bills users based on metered usage. The app is a hypothetical project management tool with a pay-as-you-go billing system based on the volume of usage (just like the Blaze plan in Firebase). A user can create multiple projects, each at a cost of $1.00 and will receive a monthly bill for the volume of projects used.
There are two new technologies (in 2018) that make a product like this possible in a single tutorial.
- Stripe Billing to make metered subscription billing easier than ever.
- Firebase Callable Functions to call HTTP endpoints without complex authentication middleware.
In addition, we will also take advantage of Firestore and Stripe Elements to quickly build a full stack subscription payment solution.
Getting Started Prerequisites
You should have the following
- Angular v6 App with AngularFire2 installed.
- A Stripe account
Inside the angular app, you should generate the following resources.
ng g service auth |
Functions Initialization
You can set functions inside your Firebase project by running:
firebase init functions |
Then go to the stripe dashboard and grab your secret key. We will set it as an environment variable in Firebase to prevent it from being exposed publicly.
You can set it as an environment variable by running:
firebase functions:config:set stripe.secret="sk_test_YOUR_KEY" |
In total, we will write three cloud functions, but they can share the following global variables in functions/src/index.ts
.
import * as functions from 'firebase-functions'; |
Add a Metered Plan in Stripe
Before writing the difficult code, let’s define a new product and metered billing plan in Stripe. Go to Billing => Products and setup a plan with metered usage.
Make a note of the plan ID - it should look like plan_xyz...
.
Setup User Auth
The first milestone is to get a Firebase user signed up with a Stripe customer ID.
This tutorial is compatible with all Firebase auth methods, but we’ll be using anonymous auth to keep the setup concise. I also recommend following Episode 55 for Google OAuth for a more in-depth look at Firebase auth.
Here’s what our user model looks like when modeled in TS.
interface User { |
Auth Service
The auth service is responsible for logging a user into our Firebase app and maintains an Observable of the user data in Firestore.
src/app/auth.service
import { Injectable } from '@angular/core'; |
Cloud Function 1: Create a Stripe Customer
The first cloud function is responsible for creating the Stripe customer. If you anticipate payments from your a users, it is helpful to setup the Stripe customer ID as soon as they signup. Conveniently, we can handle this in the background with a Firebase auth onCreate
trigger.
The following function (1) listens for a new user signup, (2) creates a stripe customer, and (3) sets the returned customer ID on that user’s document in Firestore.
functions/src/index.tsexport const createStripeCustomer = functions.auth
.user()
.onCreate(async (userRecord, context) => {
const firebaseUID = userRecord.uid;
const customer = await stripe.customers.create({
metadata: { firebaseUID }
});
return db.doc(`users/${firebaseUID}`).update({
stripeId: customer.id
});
});
Collect a Payment Source
The second major milestone is to collect a valid payment source for the user and subscribe them to the billing plan.
Add Stripe.js to index.html
Add Stripe.js to the head of the HTML index in Angular.
<head> |
Payment Form with Stripe Elements
Stripe Elements is able to mount a credit card form with complex client-side validation rules out of the box. Without elements, we would need an entire tutorial dedicated to building this form. But thankfully, we get the job done with just a few lines of code. The component needs to register Stripe with the publishable key, then give it a DOM element to attach the card form to.
The other important line of code is httpsCallable('startSubscription')
- it is making an HTTP call to a cloud function (which we create in the next step) and it will automatically include information about the user. No need to set and decode authorization headers, yay!
payment-form.component.tsimport { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
import { AngularFireFunctions } from 'angularfire2/functions';
declare var Stripe: any;
const stripe = Stripe('pk_test_xyz...');
const elements = stripe.elements();
const card = elements.create('card');
@Component(...)
export class PaymentFormComponent implements AfterViewInit {
// DOM Element
@ViewChild('cardForm') cardForm: ElementRef;
constructor(private fun: AngularFireFunctions) {}
// Mount the card form
ngAfterViewInit() {
card.mount(this.cardForm.nativeElement);
}
// Form submission Event Handler
async handleForm(e) {
e.preventDefault();
const { token, error } = await stripe.createToken(card);
const res = await this.fun
.httpsCallable('startSubscription')({ source: token.id })
.toPromise();
}
}
The HTML just needs to bind the handler to the form’s submit
event and mark a div with #cardForm
so it can be picked up by the TypeScript code.
<form action="/charge" method="post" (submit)="handleForm($event)" > |
Cloud Function 2: HTTP Callable to Start a Subscription
Now that we have collected a payment source, we need to attach it to a customer and subscribe them to a plan. Let’s break this down in steps.
- Get the user’s document from Firestore
- Add the payment source to the Stripe customer
- Subscribe the user to the billing plan
- Update the Firestore document with the plan details
functions/src/index.tsexport const startSubscription = functions.https.onCall(
async (data, context) => {
// 1. Get user data
const userId = context.auth.uid;
const userDoc = await db.doc(`users/${userId}`).get();
const user = userDoc.data();
// 2. Attach the card to the user
const source = await stripe.customers.createSource(user.stripeId, {
source: data.source
});
// 3. Subscribe the user to the plan you created in stripe
const sub = await stripe.subscriptions.create({
customer: user.stripeId,
items: [{ plan: 'plan_xyz...' }]
});
// 4. Update user document
return db.doc(`users/${userId}`).update({
status: sub.status,
currentUsage: 0,
subscriptionId: sub.id,
itemId: sub.items.data[0].id
});
}
);
Report Metered Usage
Now that our user has subscribed, we need to bill them for something. You achieve this in Stripe by sending metered usage reports, which happens to be a perfect use case for cloud functions.
In this case, users will be billed $1.00 per month for every project document they create.
If you have a high volume of events to bill, you can send usage reports periodically with the volume. For example, you might have a nightly cron job that sums up usage for the day.
Cloud Function 3: Updating Metered Plan Usage
The final step is to create a cloud function that updates Stripe with a usage report whenever data is created in Firestore.
Notice how we’re using an optional idempotency_key
to ensure that multiple function innovations do not overcharge the user. This is considered a best practice in Stripe, especially in a serverless environment.
export const updateUsage = functions.firestore |
At this point, you should be able to deploy your backend code and start testing it out.
firebase deploy --only functions |
Additional Considerations
Payments are a business critical feature there are several other considerations we have not covered, such as plan cancellation, recurring payment webhooks, and more. If you’re serious about building a payment system, checkout Full Stack Stripe Payments, which takes you through a full serverless payment solution from start to finish.