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.