Question

I finally figured out how to implement Stripes Monthly Billing using this tutorial. http://railscasts.com/episodes/288-billing-with-stripe

So far, A User can Create & Delete their Subscription with Stripe.

But how can a User change his Credit Card Information once they have created a subscription

This is my Code with Comments and Questions. Please help new to rails. :)

CONTROLLER

class SubscriptionsController < ApplicationController

  def new
    plan = Plan.find(params[:plan_id])
    @subscription = plan.subscriptions.build
    @subscription.user_id = current_user.id
  end

  def create
    @subscription = Subscription.new(params[:subscription])
    if @subscription.save_with_payment
      redirect_to @subscription, :notice => "Thank you for subscribing!"
    else
      render :new
    end
  end

  def update
    @subscription = current_user.subscription
     if @subscription.save
      redirect_to edit_subscription_path, :success => 'Updated Card.' 
    else
      flash.alert = 'Unable to update card.'
      render :edit 
    end
  end

end

MODELS

class Subscription < ActiveRecord::Base
  attr_accessible :plan_id, :user_id, :email, :stripe_customer_token, :last_4_digits,
              :card_token, :card_name, :exp_month, :exp_year, :stripe_card_token

  attr_accessor :stripe_card_token

  belongs_to :plan
  belongs_to :user

  def save_with_payment
    if valid?
      save_with_stripe_payment
    end
  end

  def save_with_stripe_payment
    customer = Stripe::Customer.create(card: stripe_card_token, email: email, plan: plan_id, description: "Unlimited Comics")
    self.stripe_customer_token = customer.id
    self.card_token = customer.cards.data.first["id"]
    self.card_name = customer.cards.data.first["type"]
    self.exp_month = customer.cards.data.first["exp_month"]
    self.exp_year = customer.cards.data.first["exp_year"]
    self.last_4_digits = customer.cards.data.first["last4"]
    save!
  rescue Stripe::InvalidRequestError => e
    logger.error "Stripe error while creating customer: #{e.message}"
    errors.add :base, "There was a problem with your credit card."
    false
  end

  def update_card
    customer = Stripe::Customer.retrieve(stripe_customer_token)
    card = customer.cards.retrieve(card_token)

    *** This Update works, but how do I pass a new Credit Card Number, Expiration Date etc.
    card.name = "My new name"
    customer.save 
  rescue Stripe::StripeError => e 
    logger.error "Stripe Error: " + e.message 
    errors.add :base, "#{e.message}." 
    false
  end

end

VIEWS

<%= form_for @subscription do |f| %>
  <%= render 'shared/error_messages', object: f.object %>

  <%= f.hidden_field :plan_id %>
  <%= f.hidden_field :user_id %>
  <%= f.hidden_field :stripe_card_token %>

  <h4>Change Credit Card</h4>

   <div class="field">
     <%= label_tag :card_number, "Credit Card Number" %>
     <%= text_field_tag :card_number, nil, name: nil %>
   </div>
   <div class="field">
     <%= label_tag :card_code, "Security Code on Card (CVV)" %>
     <%= text_field_tag :card_code, nil, name: nil %>
   </div>

   <div class="field">
     <%= label_tag :card_month, "Card Expiration" %>
     <%= select_month nil, {add_month_numbers: true}, {name: nil, id: "card_month"} %>
     <%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year+15}, {name: nil, id: "card_year"} %>
   </div>

   <%= f.submit "Change Credit Card", :class => "btn btn-primary" %>

<% end %>

ROUTES

App::Application.routes.draw do
  resources :subscriptions
end

SCHEMA

create_table "subscriptions", :force => true do |t|
  t.integer  "plan_id"
  t.integer  "user_id"
  t.string   "email"
  t.string   "card_name"
  t.string   "exp_month"
  t.string   "exp_year"
  t.string   "card_token"
  t.string   "stripe_customer_token"
  t.string   "last_4_digits"
  t.datetime "created_at",                     :null => false
  t.datetime "updated_at",                     :null => false
end
Was it helpful?

Solution

This answer has been updated thanks to the note from @TimSullivan

I had to do the same thing in my app. Stripe does not permit updating the card number. This leaves you with two options: 1) Create a new card and delete the original card; and 2) Create a new card, set it as the default_card and leave the original card. I chose the second route.

Here's how I did it:

models/subscriber.rb

def update_card(subscriber, stripe_card_token)
  customer = Stripe::Customer.retrieve(subscriber.stripe_customer_token)
  card = customer.sources.create(card: stripe_card_token)
  card.save
  customer.default_source = card.id
  customer.save
rescue Stripe::InvalidRequestError => e
  logger.error "Stripe error while updating card info: #{e.message}"
  errors.add :base, "#{e.message}"
  false
end

controllers/subscribers_controller.rb

def edit_card
  @subscriber = current_subscriber
end

def update_card
  @subscriber = current_subscriber
  if @subscriber.update_card(@subscriber, params[:stripe_card_token])
    flash[:success] = 'Saved. Your card information has been updated.'
    redirect_to @subscriber
  else
    flash[:warning] = 'Stripe reported an error while updating your card. Please try again.'
    redirect_to @subscriber
  end
end

views/subscribers/edit_card.html.erb

<%= form_for @subscriber, url: update_card_path, html: { class: 'update_subscriber' } do |f| %>
  <div class='form-group'>
    <%= label_tag        :number, 'Card Number', class: 'col-sm-3 control-label' %>
    <div class='col-sm-9'>
      <%= text_field_tag :number, nil, class: 'form-control', placeholder: 'We accept Visa, MasterCard, AMEX and Discover' %>
    </div>
  </div>

  <div class='form-group'>
    <%= label_tag        :cvc, 'Security Code', class: 'col-sm-3 control-label' %>
    <div class='col-sm-9'>
      <%= text_field_tag :cvc, nil, class: 'form-control', placeholder: 'The code on the back of your card (CVC)' %>
    </div>
  </div>

  <div class='form-group'>
    <%= label_tag        :exp_month, 'Expiration Date', class: 'col-sm-3 control-label' %>
    <div class='col-sm-5'>
      <%= select_month   nil, { add_month_numbers: true }, { id: 'exp_month', class: 'form-control btm-space' } %>
    </div>
    <div class='col-sm-4'>
      <%= select_year    nil, { start_year: Date.today.year, end_year: Date.today.year+15 }, { id: 'exp_year', class: 'form-control btm-space' } %>
    </div>
  </div>

  <div class='form-group'>
    <div class='col-sm-4 col-sm-offset-8'>
      <%= submit_tag 'Update', class: 'btn btn-success btn-block' %>
    </div>
  </div>

  <%= f.hidden_field :stripe_card_token %>
<% end %>

config/routes.rb

match '/edit_card',   to: 'subscribers#edit_card',   via: 'get'
match '/update_card', to: 'subscribers#update_card', via: 'post'

app/js/update_card.js

var subscription;

jQuery(function() {
  Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));
  return subscription.setUpForm();
});

subscription = {
  setUpForm: function() {
    return $('.update_subscriber').submit(function() {
      $('input[type="submit"]').attr('disabled', true);
      if ($('#card_number').length) {
        subscription.updateCard();
        return false;
      } else {
        return true;
      }
    });
  },
  updateCard: function() {
    var card;
    card = {
      number: $('#card_number').val(),
      cvc: $('#card_code').val(),
      expMonth: $('#card_month').val(),
      expYear: $('#card_year').val()
    };
    return Stripe.createToken(card, subscription.handleStripeResponse);
  },
  handleStripeResponse: function(status, response) {
    if (status === 200) {
      $('#subscriber_stripe_card_token').val(response.id);
      return $('.update_subscriber')[0].submit();
    } else {
      $('#stripe_error').text(response.error.message);
      return $('input[type="submit"]').attr('disabled', false);
    }
  }
};

Hope this helps.

OTHER TIPS

The accepted answer here is dangerously unsafe to use. It does an end-run around the security that Stripe encourages. Your own server should never get the credit card details.

You should instead be using Stripe.js to pass the card to Stripe directly (similar to how it is already done in the Railscast you link to) and retrieving a token, then creating the card based on the token.

def update_card(subscriber, stripe_token)
  customer = Stripe::Customer.retrieve(subscriber.stripe_customer_token)
  card = customer.cards.create(card: stripe_token)
  card.save
  customer.default_card = card.id
  customer.save
rescue Stripe::InvalidRequestError => e
  logger.error "Stripe error while updating card info: #{e.message}"
  errors.add :base, "#{e.message}"
  false
end

You can update the credit card details like this (PHP CODE)

  1. First collect the card details and the customer stripe id (that you must have saved in your database while creation of this customer).
  2. Create Stripe token and forward the same to server like this:

     Stripe.setPublishableKey('pk_test_6pRNASCoBOKtIshFeQd4XMUh');
      function stripeResponseHandler(status, response) {
      var $form = $('#payment-form');
    
      if (response.error) {
        // Show the errors on the form
        $form.find('.payment-errors').text(response.error.message);
        $form.find('button').prop('disabled', false);
      } else {
        // response contains id and card, which contains additional card details
        var token = response.id;
        alert('The Token Is: '+token);
        // Insert the token into the form so it gets submitted to the server
        $form.append($('<input type="hidden" name="stripeToken" />').val(token));
        // and submit
        $form.get(0).submit();
      }
    };
    
      jQuery(function($) {
      $('#payment-form').submit(function(event) {
        var $form = $(this);
    
        // Disable the submit button to prevent repeated clicks
        $form.find('button').prop('disabled', true);
    
        Stripe.card.createToken($form, stripeResponseHandler);
    
        // Prevent the form from submitting with the default action
        return false;
      });
    });
    
  3. and in the server you can update the details like this:

      $customer_id = trim($_REQUEST['cus_select']);     
      $query = "SELECT cus_stripe_id FROM customer_details_tbl WHERE id=$customer_id";      
      $customer_query = mysqli_query($db_conn,$query);
      $row_customer = mysqli_fetch_row($customer_query);
      $cus_stripe_id = $row_customer[0];
    
       Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");
    
       $error = '';
       $success = '';
       try {
        if (!isset($_POST['stripeToken']))
          throw new Exception("The Stripe Token was not generated correctly");
        $customer = Stripe_Customer::retrieve($cus_stripe_id);
        $customer->source = $_POST['stripeToken'];
        $customer->save();
        echo "Card Details Updated Successfully";
       }
       catch (Exception $e) {
        $error = $e->getMessage();
        echo $error;
       }
     }
    

That is it. This will overwrite the existing card with the new details.

2015-02-18 The cards and default_card attributes are no longer returned on Customers. You should now use sources and default_source respectively. The customer.card.* and customer.bank_account.* webhooks are now named customer.source.*. If you only have cards attached to customers (as opposed to payment sources of other types) then you can use the new attributes exactly as you would the old ones. If you have payment sources of multiple types, then the sources list contains heterogeneous objects and you can check the object attribute of each source to determine its format. Older API versions return both the new and the old attributes.

Use Stripe.js like Tim Sullivan suggests. Here is his code updated for the newer Stripe API.

def update_card(subscriber, stripe_token)
  customer = Stripe::Customer.retrieve({CUSTOMER_ID})
  card = customer.sources.create(card: params[:stripeToken])
  customer.default_source = card.id
  customer.save
rescue Stripe::InvalidRequestError => e
  logger.error "Stripe error while updating card info: #{e.message}"
  errors.add :base, "#{e.message}"
  false
end

So Stripe's ruby library has the update method, you already have card.name but for the expiration date you can use card.exp_month and card.exp_year, check out update method for all the arguments.

You'll notice it doesn't have an update card number on there, so my suggestion would be to have the user create another card if they want to change their credit card number and then update the customer to have that card as their default credit card (check out create card and update a customer in the stripe ruby library documentation).

Hope this helps

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top