Meteor 1.3 + React Stripe Subscription

I’m following along TheMeteorChef’s Building a SaaS with Meteor: Stripe which is built with blaze templates. Tried to use react instead but I think I failed somewhere along the way. I’ve gotten to about half of the part 1 of 2 but enough to test if signing up with plan should work or not. Well, it doesn’t work but also doesn’t give any errors in console… I have very little experience, just started actually, so I’m hoping I could get some help. Thank you.

~/client/helpers/stripe.js

Meteor.startup(function() {
  const stripeKey = Meteor.settings.public.stripe.testPublishableKey;
  Stripe.setPublishableKey(stripeKey);

  STRIPE = {
    getToken: function(domElement, card, callback) {
      Stripe.card.createToken(card, function(status, response) {
        if(response.error) {
          Bert.alert(response.error.message, "danger");
        } else {
          STRIPE.setToken(response.id, domElement, callback);
        }
      });
    },
    setToken: function(token, domElement, callback) {
      $(domElement).append($('<input type="hidden" name="stripeToken" />').val(token));
      callback();
    }
  }
});

~/client/components/SignUp.jsx

import React, {Component} from 'react';

import PlanSelectForm from '../components/PlanSelectForm.jsx';
import CreditCardForm from '../components/CreditCardForm.jsx';

export default class SignUp extends Component {
  componentDidMount() {
    $.validator.addMethod('usernameRegex', function(value, element) {
      return this.optional(element) || /^[a-zA-Z0-9-_]+$/i.test(value);
    }, "Username must contain only letters, numbers, underscores and dashes.");
    $('#application-signup').validate({
      rules: {
        username: {
          required: true,
          usernameRegex: true,
          minlength: 6
        },
        emailAddress: {
          required: true,
          email: true
        },
        password: {
          required: true,
          minlength: 6
        }
      },
      messages: {
        username: {
          required: 'You can\'t leave this empty',
          usernameRegex: 'You can use letter, numbers, underscores, and dashes.',
          minlength: 'Too short. Use at least 6 characters.'
        },
        emailAddress: {
          required: 'You can\'t leave this empty',
          email: 'Email is invalid or already taken.'
        },
        password: {
          required: 'You can\'t leave this empty',
          minlength: 'Too short. Use at least 6 characters.'
        }
      },
      handleSubmit: function() {
        STRIPE.getToken('#application-signup', {
          number: $('[data-stripe="cardNumber"]').val(),
          exp_month: $('[data-stripe="expMo"]').val(),
          exp_year: $('[data-stripe="expYr"]').val(),
          cvc: $('[data-stripe="cvc"]').val()
        }, function() {
          const customer = {
            username: $('[name="username"]').val(),
            emailAddress: $('[name="emailAddress"]').val(),
            password: $('[name="password"]').val(),
            plan: $('[name="selectPlan"]:checked').val(),
            token: $('[name="stripeToken"]').val()
          };

          const submitButton = $('input[type="submit"]').button('loading');
          
          Meteor.call('createTrialCustomer', customer, function(error, response) {
            if(error) {
              alert(error.reason);
              submitButton.button('reset');
            } else {
              if(response.error) {
                alert(response.message);
                submitButton.button('reset');
              } else {
                Meteor.loginWithPassword(customer.emailAddress, customer.password, function(error) {
                  if(error) {
                    alert(error.reason);
                    submitButton.button('reset');
                  } else {
                    Router.go('/chart');
                    submitButton.button('reset');
                  }
                });
              }
            }
          });
        });
      }
    });
  }

  render() {
    console.log(this);
    return (
      <form id="application-signup" className="signup">
        <h4>Account details</h4>
        <div className="form-group">
          <label for="username">Username</label>
          <input type="text"
                 name="username"
                 className="form-control"
                 placeholder="Username" />
        </div>
        <div className="form-group">
          <label for="emailAddress">Email Address</label>
          <input type="email"
                 name="emailAddress"
                 className="form-control"
                 placeholder="Email Address" />
        </div>
        <div className="form-group">
          <label for="password">Password</label>
          <input type="password"
                 name="password"
                 className="form-control"
                 placeholder="Password" />
        </div>
        <h4 className="page-header">Payment Information</h4>
        <label>Which plan sounds <em>amazing</em>?</label>
        <PlanSelectForm />
        <div className="form-group">
          <CreditCardForm />{/* data={signup} /> */}
        </div>
        <div className="form-group">
          <input type="submit"
                 className="btn btn-success btn-block"
                 data-loading-text="Setting up your trial..."
                 value="Put me on the rocketship" />
        </div>
      </form>
    )
  }
}

Note: In the tutorial, TheMeteorChef uses a dynamic template for CreditCardForm with data=“signup” context. I think he mentions the CC template will be used again after but I haven’t gone that far yet. Anyways, I didn’t know what “signup” means… so I left it commented out. If you know, please let me know about that as well.

~/client/components/PlanSelectForm.jsx

import React, {Component} from 'react';

export default class PlanSelectForm extends Component {
  componentDidMount() {
    const firstPlanItem = $('.select-plan a:first-child');
    firstPlanItem.addClass('active');
    firstPlanItem.find('input').prop('checked', true);
  }

  plans() {
    return Meteor.settings.public.plans;
  }

  handleClickItem(e) {
    const parent = $(e.target).closest('.list-group-item');
    console.log(parent);
    parent.addClass('active');
    $('.list-group-item').not(parent).removeClass('active');
    $('.list-group-item').not(parent).find('input[type="radio"]').prop('checked', false);
    parent.find('input[type="radio"]').prop('checked', true);
  }

  render() {
    let plans = this.plans();
    if(!plans) {
      return(<div>loading...</div>);
    }
    return (
      <div className="list-group select-plan">
        {plans.map((plan) => {
          return (
            <a key={plan.id}
               href="#"
               className="list-group-item"
               onClick={this.handleClickItem.bind(this)}>
              <input key={plan.id}
                     type="radio"
                     ref="selectPlan"
                     id={`selectPlan_${plan.id}`}
                     value={plan.name} />
              {plan.name} {plan.amount.usd}/{plan.interval}
            </a>
          )
        })}
      </div>
    )
  }
}

~/client/components/CreditCardForm.jsx

import React, {Component} from 'react';

export default class CreditCardForm extends Component {
  render() {
    return (
      <div>
        <div className="row">
          <div className="col-xs-12">
            <div className="form-group">
              <label className="text-success">
                <i className="fa fa-lock"></i> Card Number
              </label>
              <input type="text"
                     data-stripe="cardNumber"
                     className="form-control card-number"
                     placeholder="Card Number" />
            </div>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-4">
            <label>Exp. Mo.</label>
            <input type="text"
                   data-stripe="expMo"
                   className="form-control exp-month"
                   placeholder="Exp. Mo." />
          </div>
          <div className="col-xs-4">
            <label>Exp. Yr.</label>
            <input type="text"
                   data-stripe="expYr"
                   className="form-control exp-year"
                   placeholder="Exp. Yr." />
          </div>
          <div className="col-xs-4">
            <label>CVC</label>
            <input type="text"
                   data-stripe="cvc"
                   className="form-control cvc"
                   placeholder="CVC" />
          </div>
        </div>
      </div>
    )
  }
}

~/server/signup.js

Meteor.methods({
  createTrialCustomer: function(customer) {
    check(customer, {
      name: String,
      emailAddress: String,
      password: String,
      plan: String,
      token: String
    });

    const emailRegex = new RegExp(customer.emailAddress, 'i');
    const usernameRegex = new RegExp(customer.username, 'i');
    const lookupEmail = Meteor.users.findOne({'emails.address': emailRegex});
    const lookupUser = Meteor.users.findOne({'username': usernameRegex});

    if(!lookupEmail) {
      if(!lookupUser) {
        const newCustomer = new Future();

        Meteor.call('stripeCreateCustomer', customer.token, customer.emailAddress, function(error, stripeCustomer) {
          if(error) {
            console.log(error);
          } else {
            const customerId = stripeCustomer.id,
                  plan       = customer.plan;

            Meteor.call('stripeCreateSubscription', customerId, plan, function(error, response) {
              if(error) {
                console.log(error);
              } else {
                try {
                  const user = Accounts.createUser({
                    username: customer.username,
                    email: customer.emailAddress,
                    password: customer.password
                  });

                  const subscription = {
                    customerId: customerId,
                    subscription: {
                      plan: {
                        name: customer.plan,
                        used: 0
                      },
                      payment: {
                        card: {
                          type: stripeCustomer.sources.data[0].brand,
                          lastFour: stripeCustomer.sources.data[0].last4
                        },
                        nextPaymentDue: response.current_period_end
                      }
                    }
                  }
                  Meteor.users.update(user, {
                    $set: subscription
                  }, function(error, response) {
                    if(error) {
                      console.log(error);
                    } else {
                      newCustomer.return(user);
                    }
                  });
                } catch(exception) {
                  newCustomer.return(exception);
                }
              }
            });
          }
        });
        return newCustomer.wait();
      } else {
        throw new Meteor.Error('username-exists', 'Sorry, that username is already active!');
      }
    } else {
      throw new Meteor.Erro('email-exists', 'Sorry, that email is already active!')
    }
  },
})

~/server/stripe.js

const secret = Meteor.settings.private.stripe.testSecretKey;
const Stripe = StripeAPI(secret);

Meteor.methods({
  stripeCreateCustomer: function(token, email) {
    check(token, String);
    check(email, String);

    const stripeCustomer = new Future();

    Stripe.customers.create({
      source: token,
      email: email
    }, function(error, customer) {
      if(error){
        stripeCustomer.return(error);
      } else {
        stripeCustomer.return(customer);
      }
    });

    return stripeCustomer.wait();
  },

  stripeCreateSubscription: function(customer, plan) {
    check(customer, String);
    check(plan, String);

    const stripeSubscription = new Future();

    Stripe.customers.createSubscription(customer, {
      plan: plan
    }, function(error, subscription) {
      if(error) {
        stripeSubscription.return(error);
      } else {
        stripeSubscription.return(subscription);
      }
    });

    return stripeSubscription.wait();
  }
})

packages

  "dependencies": {
    "meteor-node-stubs": "~0.2.0",
    "react": "^15.0.2",
    "react-addons-css-transition-group": "^15.0.2",
    "react-dom": "^15.0.2",
    "react-mounter": "^1.2.0"
  },



accounts-base
accounts-password
session
check
random

kadira:flow-router
ultimatejs:tracker-react
meteortoys:allthings

fourseven:scss
fortawesome:fontawesome
themeteorchef:bert
themeteorchef:jquery-validation
momentjs:moment

mrgalaxy:stripe

Thanks for reading, I hope that it wasn’t painful.

1 Like

Sorry to bump but I’m in desperate need of help.

Did you ever get this working? I am currently looking for a solution.
Thanks

https://themeteorchef.com/tutorials/subscriptions-with-stripe

1 Like

I just followed the excellent Meteor Chef tutorial linked above and this article everything worked fine.