Stripe Subscriptions: Duplicate Customers
Hey Eeveryone,
I'm creating a subscription site using Stripe v3. For the most part everything is working fine, but I noticed in my Stripe Customer dashboard there are duplicate customers (for all customers). The first customer does not have a card associated with it, but the duplicate customer does.
I noticed in my logs that it is performing an update to the member two times right in a row, but I can't figure out what's causing this to happen. Pasted logs at the very bottom.
Here's all my stuff
I know it's a lot to look through, but I really appreciate any help!
Member Model
def stripe_customer
if stripe_id?
Stripe::Customer.retrieve(stripe_id)
else
stripe_customer = Stripe::Customer.create(email: email)
update(stripe_id: stripe_customer.id)
stripe_customer
end
end
Memberships Controller
before_action :authenticate_member!
def new
end
def create
customer = current_member.stripe_customer
begin
subscription = customer.subscriptions.create(
source: params[:stripeToken],
plan: params[:plan]
)
current_member.assign_attributes(stripe_subscription_id: subscription.id, expires_at: nil)
current_member.assign_attributes(
card_brand: params[:card_brand],
card_last4: params[:card_last4],
card_exp_month: params[:card_exp_month],
card_exp_year: params[:card_exp_year]
) if params[:card_last4]
current_member.save
flash.notice = "BooYah!!! Thanks for signing up!"
redirect_to edit_member_registration_path(current_member)
rescue Stripe::CardError => e
flash.alert = e.message
render action: :new
end
end
def show
end
Stripe Elements
document.addEventListener("turbolinks:load", function() {
var public_key = document.querySelector("meta[name='stripe-public-key']").content;
var stripe = Stripe(public_key);
var elements = stripe.elements();
// Custom styling can be passed to options when creating an Element.
var style = {
base: {
// Add your base input styles here. For example:
fontSize: '16px',
lineHeight: '24px'
}
};
// Create an instance of the card Element
var card = elements.create('card', {style: style});
// Add an instance of the card Element into the `card-element` <div>
card.mount('#card-element');
card.addEventListener('change', function(event) {
var displayError = document.getElementById('card-errors');
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
});
// Create a token or display an error when the form is submitted.
var form = document.getElementById('payment-form');
form.addEventListener('submit', function(event) {
event.preventDefault();
stripe.createToken(card).then(function(result) {
if (result.error) {
// Inform the user if there was an error
var errorElement = document.getElementById('card-errors');
errorElement.textContent = result.error.message;
} else {
// Send the token to your server
stripeTokenHandler(result.token);
}
});
});
});
function stripeTokenHandler(token) {
// Insert the token ID into the form so it gets submitted to the server
var form = document.getElementById('payment-form');
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'stripeToken');
hiddenInput.setAttribute('value', token.id);
form.appendChild(hiddenInput);
["brand", "exp_month", "exp_year", "last4"].forEach(function(field) {
addFieldToForm(form, token, field);
});
// Submit the form
form.submit();
}
function addFieldToForm(form, token, field) {
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', "card_" + field);
hiddenInput.setAttribute('value', token.card[field]);
form.appendChild(hiddenInput);
}
Logs
Started POST "/membership" for 127.0.0.1 at 2017-09-03 14:04:15 -0500
Processing by MembershipsController#create as JS
Parameters: {"utf8"=>"✓", "authenticity_token"=>"tF7nwOkJPALmvzrH5UVC4TgxolKBYH8pRQYkVpK8tSFG8ADR4dQnJmWHYBYSuQq2pI8OdvtXvqgIIoRQ3/fIXA==", "plan"=>"annual"}
Member Load (0.6ms) SELECT "members".* FROM "members" WHERE "members"."id" = $1 ORDER BY "members"."id" ASC LIMIT $2 [["id", 15], ["LIMIT", 1]]
Started POST "/membership" for 127.0.0.1 at 2017-09-03 14:04:16 -0500
Processing by MembershipsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"tF7nwOkJPALmvzrH5UVC4TgxolKBYH8pRQYkVpK8tSFG8ADR4dQnJmWHYBYSuQq2pI8OdvtXvqgIIoRQ3/fIXA==", "plan"=>"annual", "stripeToken"=>"tok_1Ay3OGFjHiphw0iAWDy0deos", "card_brand"=>"Visa", "card_exp_month"=>"12", "card_exp_year"=>"2023", "card_last4"=>"0002"}
Member Load (0.6ms) SELECT "members".* FROM "members" WHERE "members"."id" = $1 ORDER BY "members"."id" ASC LIMIT $2 [["id", 15], ["LIMIT", 1]]
|==This where I think the duplication is coming from. But I can't figure out why or what's causing it
==================================================================================
(0.2ms) BEGIN
SQL (0.7ms) UPDATE "members" SET "stripe_id" = $1, "updated_at" = $2 WHERE "members"."id" = $3 [["stripe_id", "cus_BKipFeUFJtHX6t"], ["updated_at", "2017-09-03 19:04:17.125942"], ["id", 15]]
(6.6ms) COMMIT
(0.2ms) BEGIN
SQL (0.6ms) UPDATE "members" SET "stripe_id" = $1, "updated_at" = $2 WHERE "members"."id" = $3 [["stripe_id", "cus_BKipmnHyvuuvVH"], ["updated_at", "2017-09-03 19:04:17.252095"], ["id", 15]]
(0.5ms) COMMIT
==================================================================================
Completed 500 Internal Server Error in 1995ms (ActiveRecord: 2.0ms)
Stripe::InvalidRequestError (This customer has no attached payment source):
app/controllers/concerns/memberships_controller.rb:11:in `create'
Rendering memberships/new.html.erb within layouts/application
Rendered memberships/new.html.erb within layouts/application (0.9ms)
Rendered shared/_flash.html.erb (0.8ms)
Completed 200 OK in 1625ms (Views: 59.6ms | ActiveRecord: 8.2ms)
Also, I'm not sure why I'm seeing these two lines in my log:
Completed 500 Internal Server Error in 1995ms (ActiveRecord: 2.0ms)
Stripe::InvalidRequestError (This customer has no attached payment source):
Again, I know this is a lot to go through, but I really appreciate any help anyone can offer!
Please let me know if anyone needs any additional info!
Thanks!
Nick
Hey Nick,
That's sure weird! I think what it looks like is happening is that your Javascript is submitting the form twice.
You can tell because the first POST to /memberships hasn't finished before the second one starts. And that's why the logs are mixed together and you have those two UPDATEs on the user. They're processing almost at exactly the same time.
You should do some debugging on your Javascript to figure out why it's executing twice and that should fix the problem.
Thanks Chris,
I was getting some erros from rails-ujs, so I swapped if for jquery-ujs since I'm using jquery-rails and that seems to have fixed the dupicate customer problem.
I'm still getting some Stripe releated errors that I'm looking into now. I may endup opening another thread with more detail, but in short I keep getting an error saying (paraphrased) :
#card-element can't be found and to make sure it's on the page before calling .mount
#card-element
is definitely on my form view, so I'm assuming it has to do with the order of my js being loaded or turbolinks or both.
Has anyone experienced this? I'm using Rails 5.1.x.
Thanks!
The #card-element
error comes from you trying to run this on every page regardless if the field is on the page. You'll want to add some JS to check if the element exists, and if it does not, return.
document.addEventListener("turbolinks:load", function() {
if (!document.querySelector("#card-element")) {
return
}
// rest of your code
}
Hey Everyone,
Just wanted to give a quick update. So, I was still getting some really weird behavior like, duplicate customers and resubscribing was creating 4 new subscriptions!
I was using a form_with
tag for the payment forms. I changed that to a form_tag
and now everything is working as expected.
Not sure why the form_with
was causing all this disaray, maybe I was using it wrong... I don't know. I do know changing it to a form_tag
fixed it.
Has anyone dealt with this before or have any idea what was going on with the form_with
helper?
Thanks!
Ummm... No, no I did not have local: true
on the form_with
.
Man!!! How did I miss that!!!
By default form_with attaches the data-remote attribute submitting the form via an XMLHTTPRequest in the background if an Unobtrusive JavaScript driver, like rails-ujs, is used. See the :local option for more. source: http://edgeapi.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-formwith
Thanks, Chris! You really are the man!! Appreciate all your help!!!
I wonder if that got triggered along with the JS for Stripe. May need to update my screencasts to mention that if I didn't cover that properly.
Can't we just have nice things that work out of the box?? 😜
Haha, I know, right!!!
Actually, in the Stripe Elements screencast you use a form_tag
because you're using Rails 5.1.0. You attempt to use the form_with
, realize you can't and then move on with the form_tag
.
I ran into this issue when going through the same course recently on Rails 5.2. local: true definitely fixed it. Thanks guys!
Thomas, the Stripe series is new a separate course from GoRails: https://courses.gorails.com/payments-with-rails-master-class
It covers the latest Stripe with a shopping cart example for one-time payments, Stripe Billing subscriptions. Soon it will have Braintree + PayPal examples in the course as well.
Yes, I know it's a separate course, which I think is kind of unfair to us long-time subscribers, because there are some problems with the instructions that subscribers have access to. One of them dogged me for a couple of days until the folks at Stripe pointed out that using Bootstrap to decorate the Elements mount would cause it to not be visible on the page. And the other issues as pointed out in this thread. You mention yourself that these things get out of date quickly. Thank you.
Minor bits and pieces can get out of date but to be honest, Stripe's documentation is really good and their engineering team is great about helping users fix issues like you've had. I don't think it's unreasonable for Chris to have separate Master Classes. I've been a GoRails subscriber for a while and I purchased the course and have used it on a few projects now.