Rails Stripe Subscriptions, Webhooks, and Payola

662 views Asked by At

I am currently using rails-stripe-membership-saas to set up my SaaS site. I currently have plans set up in Stripe that corresponds with rails to sign users up for a certain plan, all of which have a 14-day trial.

I need help figuring out how to not require the user to input their credit card on sign-up and then use a Stripe webhook to send them a request or somehow alert the user that they need to put their credit card in to continue their subscription or else they won't be able to log back in.

Whenever I remove the code from the form for requiring the cards, the form stops working - and if even if it worked, I'm not sure how to implement the stripe webhooks, which I believe I may have to use event-stripe gem for.

If anyone could point me in the right direction it would be much appreciated. If any other code/information is required, just let me know and I'll be sure to post it.

Here is the form I am using to sign users up: registrations/new.html.erb

  <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :role => 'form',
            :class => 'payola-onestep-subscription-form',
            'data-payola-base-path' => payola_path,
            'data-payola-plan-type' => resource.plan.plan_class,
            'data-payola-plan-id' => resource.plan.id}) do |f| %>
              <div>
                <span id="error_explanation" class="payola-payment-error"></span>
              </div>
              <div class="form-group">
              <h3 class="text-center"><i>Try Free for 14 Days!</i></h3> <br>
                <%= f.label 'Subscription plan' %>
                <%= f.collection_select(:plan_id, Plan.all, :id, :name) %>
              </div>
              <div class="form-group">
                <%= f.label :email %>
                <%= f.email_field :email, :autofocus => true, class: 'form-control', data: { payola: 'email' }  %>
              </div>
              <div class="form-group">
                <%= f.label :password %>
                <%= f.password_field :password, class: 'form-control' %>
              </div>
              <div class="form-group">
                <%= f.label :password_confirmation %>
                <%= f.password_field :password_confirmation, class: 'form-control' %>
              </div>
              <div class="form-group">
                <%= label_tag :card_number, "Credit Card Number" %>
                <%= text_field_tag :card_number, nil, name: nil, class: 'form-control', data: { stripe: 'number' } %>
              </div>
              <div class="form-group">
                <%= label_tag :card_code, "Card Security Code" %>
                <%= text_field_tag :card_code, nil, name: nil, class: 'form-control', data: { stripe: 'cvc' } %>
              </div>
              <br />
              <div class="form-group">
                <%= label_tag :card_month, "Card Expiry" %>
                <%= select_month nil, { use_two_digit_numbers: true}, { name: nil, data: { stripe: 'exp-month' } } %>
                <%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year+10}, { name: nil, data: { stripe: 'exp-year' } } %>
              </div>
              <div class="text-center">
              <%= f.submit 'Sign up', :class => 'button right' %>
              </div>
            <% end %>

Here are the plans that I currently have laid out.

class CreatePlanService
  def call
    p1 = Plan.where(name: 'Yearly').first_or_initialize do |p|
      p.amount = 36000
      p.interval = 'year'
      p.stripe_id = 'yearly'
    end
    p1.save!(:validate => false)
    p2 = Plan.where(name: 'Monthly').first_or_initialize do |p|
      p.amount = 3000
      p.interval = 'month'
      p.stripe_id = 'monthly'
    end
    p2.save!(:validate => false)
  end
end

User model

class User < ActiveRecord::Base
  enum role: [:user, :admin, :yearly, :monthly]
  after_initialize :set_default_role, :if => :new_record?
  after_initialize :set_default_plan, :if => :new_record?
  # after_create :sign_up_for_mailing_list

  belongs_to :plan
  validates_associated :plan

  has_many :dashboards
  has_many :cardtools

  def set_default_role
    self.role ||= :user
  end

  def set_default_plan
    self.plan ||= Plan.last
  end

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  def sign_up_for_mailing_list
    MailingListSignupJob.perform_later(self)
  end

  def subscribe
    mailchimp = Gibbon::Request.new(api_key: Rails.application.secrets.mailchimp_api_key)
    list_id = Rails.application.secrets.mailchimp_list_id
    result = mailchimp.lists(list_id).members.create(
      body: {
        email_address: self.email,
        status: 'subscribed'
    })
    Rails.logger.info("Subscribed #{self.email} to MailChimp") if result
  end


end

Registrations controller

class RegistrationsController < Devise::RegistrationsController
  include Payola::StatusBehavior
  before_action :cancel_subscription, only: [:destroy]

  def new
    build_resource({})
    unless params[:plan].nil?
      # If broken, follow console https://github.com/RailsApps/rails-stripe-membership-saas/issues/127
      @plan = Plan.find_by!(stripe_id: params[:plan])
      resource.plan = @plan
    end
    yield resource if block_given?
    respond_with self.resource
  end

  def create
    build_resource(sign_up_params)
    plan = Plan.find_by!(id: params[:user][:plan_id].to_i)
    resource.role = User.roles[plan.stripe_id] unless resource.admin?
    resource.save
    yield resource if block_given?
    if resource.persisted?
      if resource.active_for_authentication?
        set_flash_message :notice, :signed_up if is_flashing_format?
        sign_up(resource_name, resource)
        subscribe
      else
        set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
        expire_data_after_sign_in!
        subscribe
      end
    else
      clean_up_passwords resource
      render json:
        {error: resource.errors.full_messages.to_sentence},
        status: 400
    end
  end

  def change_plan
    plan = Plan.find_by!(id: params[:user][:plan_id].to_i)
    unless plan == current_user.plan
      role = User.roles[plan.stripe_id]
      if current_user.update_attributes!(plan: plan, role: role)
        subscription = Payola::Subscription.find_by!(owner_id: current_user.id)
        Payola::ChangeSubscriptionPlan.call(subscription, plan)
        redirect_to edit_user_registration_path, :notice => "Plan changed."
      else
        flash[:alert] = 'Unable to change plan.'
        build_resource
        render :edit
      end
    end
  end

  private

  def sign_up_params
    params.require(:user).permit(:email, :password, :password_confirmation, :plan_id)
  end

  def subscribe
    return if resource.admin?
    params[:plan] = current_user.plan
    subscription = Payola::CreateSubscription.call(params, current_user)
    current_user.save
    render_payola_status(subscription)
  end

  def cancel_subscription
    subscription = Payola::Subscription.find_by!(owner_id: current_user.id, state: 'active')
    Payola::CancelSubscription.call(subscription)
  end

end
1

There are 1 answers

1
Afolabi Olaoluwa On BEST ANSWER

Removing the Card Attribute from that form definite will make it crash.

Payola assumes you want to create an immediately charging subscription. This means free plans and plans with free trials require a credit card at sign up. So getting around this might be:

  1. Using stripe-rails gem instead of the payola library. Stripe doesn't require a card for free plans or plans with a trial.

OR

  1. You follow Payola creator's workaround on this. https://github.com/peterkeen/payola/commit/0b0282fb256eba7247999fbf2b33f2ded567c311. Though might be Head on Rock for you.

As for webhooks, I see that possible. When a credit card expires or a monthly transaction is declined, Stripe will automatically retry a recurring payment after it fails. After a number of attempts (set in your Stripe account settings), Stripe will cancel the subscription. Your application needs to know to deny access for a subscriber with an expired account. Stripe provides webhooks to communicate events to you (for details, see the Stripe Webhooks Documentation).

A Stripe webhook is an HTTP request from Stripe’s servers to your site, containing JSON data that provides data about the event, plus an event id that can be used to retrieve the data from the Stripe server. The example application responds to Stripe webhooks, using an implementation provided by Danny Whalen’s stripe_event gem, which is provided with the Payola gem. The application responds to webhook requests at https://www.example.com/payola/events.

The example application only responds to “customer.subscription.deleted” events. You can customize the application to respond to other events (such as sending a thank you email in response to an “invoice.payment_succeeded” event).

For webhooks to work, you must visit your Stripe dashboard at https://manage.stripe.com/#account/webhooks and add the URL for your application, such as https://www.example.com/payola/events.