Best practice for showing/hiding premium features based on Stripe subscription

Hello community.

I am in the process of adding some premium features to my Meteor web app using Stripe, so far so good.

My plan is to have an accountType on the User document set to ‘FREE’ + ‘PREMIUM’, along with the relevant Stripe Ids etc, which is altered via stripe webhooks.

What is the best way to show/hide premium features based on a users Stripe status?

The Meteor Chef’s very helpful guide use a server method to set a session var in order to toggle the visibility of elements based on subscription.

However, a particularly crafty user could just set this themselves in the console and calling the method every single time i want to render stuff seems like a bad idea too.

Would love to learn from the community about the best way to implement this

Thanks
Robin

On my applications, I have a user property called isSubscribed That I have to publish to make sure I see it in the client.

Meteor.publish('userExtraFields', () => {
  const _id = Meteor.userId();
  if (_id) {
    return Meteor.users.find(
      { _id },
      {
        fields: {
          isSubscribed: 1,
          key: 1,
          stripe: 1,
          version: 1,
        },
      },
    );
  }
  return false;
});

Since it’s not in the user.profile the user has no right to modify it with the client Meteor.users methods and you can access it from blaze with {{#if currentUser.isSubscribed}}...{{else}}...{{/if}}

PS: as you can see I also store the stripe object into user because, why not :slight_smile:

2 Likes

nice. this solution is elegant and works well, thanks for your quick reply!

1 Like

I think you might want to look into the Roles package as one option:

4 Likes

Hi. First of all, congrats on getting your app to a stage where you feel selling subscriptions is in order!

Regarding implementation, do keep in mind that every bit of data in client side can be modified by the user, regardless of how you store it. So consider any ‘isSubsribed’ or any other similar flag that you have in your client side code just a convenience thing to be able to show the right kind of UI to the user. The actual secure check of this flag should happen on the server, presumably in your methods or publications. For example, you only return certain data in methods on the server if that flag is set to true.

As long as you make those checks server side, it does not make much difference where exactly you store such flags (e.g. in a field in your users collection) and how you transport those flags to the client (e.g. method or publication that’s run when a client connects).

But I’d say take a minute to think about whether a single boolean is sufficient for you in the long term. Might be that you want to introduce different plans (Prices, as the new Stripe api calls these IIRC) and therefore you might want to have that data available on the client as well.

3 Likes

Hey! We don’t do it anymore, and I am sure there may be a cleaner way to do it; however, we would fire something like this on sign in (on the client using svelte…but would be the same for Blaze etc):

Accounts.onLogin(function(){
  	(async function(){
    const customer = await MethodWithPromise('StripeRetrieveCheckSubscription', Meteor.userId());
    if(customer){
      if(customer.deleted){
        handleCustomerSub('canceled');
      } else{
        const subArr = customer.subscriptions.data;
        if(subArr && subArr.length > 0){
          const subState = subArr[0].status;
          if(subState){
            if(subState === 'canceled'){
              handleCustomerSub('canceled');
            } else if(subState === 'active') {
              handleCustomerSub('active');
            }
          } else{
            handleCustomerSub('canceled');
          }
        } else{
          handleCustomerSub('canceled');
        }
      }
    }
  })();
});

The ‘handleCustomerSub’ in a nutshell just fires a method that updates the DB status for the subscription and the method to check the subscription with Stripe is pretty simple:

  StripeRetrieveCheckSubscription: (userId) => {
    if(userId){
      const user = Meteor.users.findOne({_id:userId});

      if(user){
        const companyId = user.profile.company;
        const customerId = Companies.findOne({_id:companyId}).stripeCustomerId;
    
        if(customerId){
          return stripe.customers.retrieve(
            customerId
          );
        }
      }
    }
  },

Probably many ways of doing this…but this general concept worked for us! :slight_smile:

No matter if it’s a crafty user or not, server-side authorization is always still your best defense. We don’t allow any user to modify their profile from the client, which is our first defense. But even if a user can sniff out the appropriate Method or Publication, we have our authorization guards on the server that prevent any payload going out unless they have the right privileges.

In our app, we have a notion of Spaces, where users can be part of a collaborative group of others. When a user sets up multiple Spaces with others, they can switch between them. Since we don’t want users trying to “guess” a Space they’re not part of, here’s an example Method we use to keep them in line:

spaceSwitch: function(id) {
    if (!this.userId) { // authentication check 
        return; // if not logged in, just bail immediately 
    }
    check(id, String); // assertion. this is the id of the space they're switching to and it better be a String
    var user = Meteor.user();
    if(user.allSpaces.indexOf(id) === -1){ // authorization check: if the id they're switching to isn't in their array of existing spaces... 
        return; // then bugger off 
    }
    Meteor.users.update({_id: this.userId}, {$set: {'space': id, 'profile.space': id}});
    return 'switched';
}

One note on the publish function above: instead of returning all documents with the isSubscribed field, if you were trying to restrict access to the Publication based on that property, one modification could be this:

Meteor.publish('userExtraFields', () => {
    var user = Meteor.users.findOne({_id: this.userId});
    if (user && user.isSubscribed) { // don't bother sending documents to the client unless this is true
        return Meteor.users.find(
            { _id },
            {
                fields: {
                    isSubscribed: 1,
                    key: 1,
                    stripe: 1,
                    version: 1,
                },
            },
        );
    }
    return false;
});

I’m assuming that function is written to serve that app’s use-case, but just wanted to point this out for @robinhunter27 as an example to restrict access at the Method or Publication level.

1 Like

Thanks all for the thoughtful responses and guidance.

Okay interesting,

So you save a Stripe Customer ID in the companies collection

Then every time a user logs in, we check the status of that stripe account and update the DB via ‘handlecustomersub’

Rather than using a web hook that stripe interacts with?

Thanks

1 Like

Yes :slight_smile: that is what we were doing.

We would not advise saving the stripe data directly to the users DB document just for security reasons, and to reiterate what others have said, we handle everything server-side, and simply use a basic promise on a method to ensure the data comes back from the server before changing/doing anything on the client.

For us though, this process worked pretty well at verifying whether or not the user’s company account was up to date. For users with admin privileges we fired an alert (we use Sweet Alert 2 See Here) that tells them that there is a billing issue, and for non-admin users, we do the same but just tell them to check with their admins.

You can wrap your routes or views in a simple conditional to only display if your stripe status is x,y, or z…and viola you have a fairly simple solution. :slight_smile:

Good luck!