Simple subscription payments with Meteor & Braintree

Taking payments on any SaaS app is super important, fortunately with Meteor and Braintree it's not too difficult, but there are a couple of tricky parts.

Step 1: Go get a Braintree account

The first $50k of payments is free (I think), hence why I chose it over stripe.

Step 2: Set up your plans in the Braintree Sandbox & Production

See instructions here

Step 3: Add your keys (sandbox and production) to your settings.json files

 "braintree": {
   "merchantId": "",
   "publicKey": "",
   "privateKey": ""
 }

You'll need these later. Alternatively you can use environment variables or whatever you prefer to inject runtime vars into your app. If you followed our deployment guide this should be super easy.

Step 4: Create your pricing page, routes and purchase completed page

I won't cover these here, as there are a million ways you could do this.

Briefly, we have a single pricing page and use a separate route for each plan, we store the chosen plan as a session variable and we use sweet alert to notify the user on success or payment failure.

Step 5: Add the drop in Braintree payments form

Install the braintree web tools:
meteor npm install braintree-web

Now create a template with the braintree form and place it wherever you want to collect payments in your flow:

{{#if paymentProcessing}}
  {{ >loading}}
{{else}}
  <form id="checkout" method="post">
    <div id="payment-form"></div>
      <button type="submit" class="btn btn-primary btn-block">Subscribe</button>
  </form>
{{/if}}

We have to do some setup to make this work:

  1. On creating the form, get our client token from our server
  2. Setup the braintree form with the client token
  3. Respond to submitted payment form using the paymentMethodNonceReceived(event, nonce)
  4. Finally notify the user that they were successful in subscribing (or not)

This is covered in the below handlers, in the next step we'll look at the server side methods required to make this work:

import { Template } from 'meteor/templating';  
import { Session } from 'meteor/session';  
import { Meteor } from 'meteor/meteor';  
import braintree from 'braintree-web';  
import { Router } from 'meteor/iron:router';

Template.subscribe.helpers({  
  plan() {
    return Session.get('plan');
  },
  paymentProcessing() {
    return Session.get('paymentProcessing');
  },
});

Template.subscribe.onCreated(() => {  
  Meteor.call('getClientToken', (err, clientToken) => {
    if (err) {
      throw new Meteor.Error(err.statusCode, 'Error getting client token from braintree');
    }

    braintree.setup(clientToken, 'dropin', {
      container: 'payment-form',
      paymentMethodNonceReceived(event, nonce) {
        Session.set('paymentProcessing', true);
        const plan = Session.get('plan');
        Meteor.call('subscribeToPlan', nonce, plan, (error, result) => {
          Session.set('paymentProcessing', false);
          if (error) {
            sweetAlert(error.message, 'error');
          } else {
            sweetAlert({
              title: 'Subscription Successful',
              text: `You are now subscribed to the ${plan} plan`,
              type: 'success',
            }, () => {
              Router.go('apps');
            });
          }
        });
      },
    });
  });
});

Note that I'm storing the name of my plan in the plan session variable and the status of the payment form in the paymentProcessing session var

Step 6: Create the server side methods to speak with the braintree API

As you can see in the above, there are two methods we need to define on the server:

  • getClientToken(callback)
  • subscribeToPlan(none, plan, callback)

You'll need the braintree node library, so start with that:

$ meteor npm install braintree

Now setup the methods server side. Firstly, get our environment details and import the required packages (we'll use the Roles package to control what users have access to):

import braintree from 'braintree';  
import { Meteor } from 'meteor/meteor';  
import { Roles } from 'meteor/alanning:roles';

let env = braintree.Environment.Sandbox;

if (Meteor.settings.environment === 'production') {  
  env = braintree.Environment.Production;
}

const gateway = braintree.connect({  
  environment: env,
  merchantId: Meteor.settings.braintree.merchantId,
  publicKey: Meteor.settings.braintree.publicKey,
  privateKey: Meteor.settings.braintree.privateKey,
});

Next let's set up our methods (in the same file).

getClientToken(clientId) is quite straightforward:

  • Prepares a generateToken function by taking the standard node async method and making it run synchronously
  • Calls the generateToken method using your clientId (optional)
  • Returns the token for setting up the html form

subscribeToPlan(nonce, plan) is a little more complex:

  • Prepares the customer, subscription and plans methods
  • Obtains all your plans from the braintree API and gets the id of the plan
  • Creates a new braintree customer
  • If the customer creation is successful, creates a new subscription, using the plan and payment nonce
  • If the subscription is successful, sets the role of user to match the plan they subscribed to
Meteor.methods({  
  getClientToken: (clientId) => {
    const generateToken = Meteor.wrapAsync(gateway.clientToken.generate, gateway.clientToken);
    const options = {};

    if (clientId) {
      options.clientId = clientId;
    }

    const response = generateToken(options);
    return response.clientToken;
  },
  subscribeToPlan(nonce, plan) {
      const customer = Meteor.wrapAsync(gateway.customer.create, gateway.customer);
      const subscription = Meteor.wrapAsync(gateway.subscription.create, gateway.subscription);
      const plans = Meteor.wrapAsync(gateway.plan.all, gateway.plan);
      const plansResult = plans().plans;

      const planId = plansResult.find((planObject) => {
        return planObject.name === plan;
      }).id;

      const email = Meteor.user().emails[0].address;

      const customerResult = customer({
        email,
        paymentMethodNonce: nonce,
      });

      if (customerResult.success) {
        const token = customerResult.customer.paymentMethods[0].token;
        const subscriptionResult = subscription({
          paymentMethodToken: token,
          planId,
        });

        if (subscriptionResult.success) {
          // Set / check the correct role for your user
          const currentRoles = Roles.getRolesForUser(Meteor.userId());
          currentRoles.forEach((role) => {
            if (role === plan) {
              throw new Meteor.Error('400', 'User already subscribed to this plan');
            } else {

              // We support 3 roles, but you may have more or less (it's probably better to get these from the braintree api)
              if (role === 'free' || role === 'developer' || role === 'professional') {
                Roles.removeUsersFromRoles(Meteor.userId(), role);
              }
            }
          });

          // add new subscription
          Roles.addUsersToRoles(Meteor.userId(), plan);

          return true;
        }
      }
  },
});

Note, we still need to check if the customer is already subscribed, and reuse their braintree details if so, and also turn off any old subs, which we can do with the braintree API but I won't cover here here.

Step 7. Use your roles to hide/show features

You can now turn features on and off on an account basis, for example, this is how we control whether to show a button to add more apps (you also need to check this server side):

Template.apps.helpers({  
  maxAppsReached() {
    let maxApps = 1;
    if (Roles.userIsInRole(Meteor.userId(), 'developer')) {
      maxApps = 5;
    }
    if (Roles.userIsInRole(Meteor.userId(), 'professional')) {
      maxApps = 10000;
    }
    return Apps.find().count() >= maxApps;
  },
});

Oh, and the last step, don't forget to configure your trombone account ;-)