Paypal & Stripe
Hi Chris - I just finished your Stripe course, which works great, but my client also wants the PayPal option. I am just wondering if you could share some insight on how you did it on your site?
Also, it would be great if you could update your Stripe course to include the pay now buttton integration. https://stripe-payments-demo.appspot.com/
Thank you,
Brian
Hey Brian,
So for PayPal, I actually use Braintree. I've got it setup so I don't use their form, but rather trigger the PayPal checkout from a link. You can check out the Braintree episode I did on GoRails for that. I am using the Braintree JS v2 still, as it looks like they came out with v3 that doesn't support the same feature.
https://gorails.com/episodes/braintree
It's just a lot easier to integrate with Braintree than it is to use PayPal directly, and PayPal purchased Braintree a few years ago.
Thanks Chris - I just subscribed, and I will watch that eposide.
One last question if you don't mind. I am on this eposide of your stripe course "Resubscribing using an existing card" but I am having a slight problem.
My form is still displaying for users with an existing credit card on file with this set up:
<%= form_with url: subscription_path, id: "payment-form", style: (current_user.card_last4? ? "display:none" : nil) do |form| %>
Do you know what I am doing wrong here?
Complete file: (Subscriptions.new.html.erb)
<h1>Subscribe</h1>
<% if current_user.card_last4? %>
<%= form_with url: subscription_path, id: "existing-card" do |form| %>
<p>Pay with existing card</p>
<div><strong>Card on file:</strong> <%= current_user.card_brand %> (**** **** **** <%= current_user.card_last4 %>)</div>
<div><strong>Expiration:</strong> <%= current_user.card_exp_month %> / <%= current_user.card_exp_year %></div>
<p>or <%= link_to "Add a new card", "#", class: "show-card-form" %></p>
<%= hidden_field_tag :plan, params[:plan] %>
<button class="btn btn-primary">Submit Payment</button>
<% end %>
<% end %>
<%= form_with url: subscription_path, id: "payment-form", style: (current_user.card_last4? ? "display:none" : nil) do |form| %>
<div class="form-row">
<label for="card-element">
Credit or debit card
</label>
<div id="card-element" class="form-control">
<!-- A Stripe Element will be inserted here. -->
</div>
<!-- Used to display Element errors. -->
<div id="card-errors" role="alert"></div>
</div>
<%= hidden_field_tag :plan, params[:plan] %>
<button class="btn btn-primary">Submit Payment</button>
<% end %>
Hey Brian,
So the existing card form shows up but it doesn't hide the new card form? Is that correct?
For some debugging, just print out the results of things like <%= current_user.card_last4? %>
to make sure it is true
. Then try just adding a simple like style: "background: blue"
to your form and make sure you've got that working. Then go swap it out with the conditional version, since you'll probably have discovered what's wrong then.
After some testing and digging around, I figured it out. I think it was due to the fact that I was using form_with instead of form_tag.
Maybe form_with doesn't accept a style tag anymore? My fix was just to change style to class, and then add the none class to my css.
<%= form_with url: subscription_path, id: "payment-form", class: (current_user.card_last4? ? "none" : nil) do |form| %>
Thanks for you help with this.
Looks like it doesn't support it directly, it must be nested inside an html
key. You can see under the "form_with options" header here: https://api.rubyonrails.org/v5.1/classes/ActionView/Helpers/FormHelper.html#method-i-form_with
Either way, that works fine though. 👍
Hi Chris - Can I bug you for one last question? I am getting a no method error for amount in the rails console after I run:
e = Stripe::Event.retrieve("evt_1D29rN2OobZT8tZuHzzw9Zbp")
Webhooks::ChargeSucceeded.new.call(e)
NoMethodError (undefined method `amount' for #Stripe::Subscription:0x00005630b03da5b8)
Amount is set in my database so I am not sure what's going on.
module Webhooks
class ChargeSucceeded
def call(event)
charge = event.data.object
user = User.find_by(stripe_id: charge.customer)
c = user.charges.where(stripe_id: charge.id).first_or_create
c.update(
amount: charge.amount,
card_brand: charge.source.brand,
card_last4: charge.source.last4,
card_exp_month: charge.source.exp_month,
card_exp_year: charge.source.exp_year
)
end
end
end
class CreateCharges < ActiveRecord::Migration[5.2]
def change
create_table :charges do |t|
t.references :user, foreign_key: true
t.string :stripe_id
t.integer :amount
t.string :card_brand
t.string :card_last4
t.string :card_exp_month
t.string :card_exp_year
t.timestamps
end
end
end
Here is a dropbox link to my application: https://www.dropbox.com/s/xj9ul4lbj118mh5/stripe.zip?dl=0
You're calling charge.amount
on the stripe object you set just before with:
charge = event.data.object
Since that's a Subscription object, you can call methods on it for the attributes it has. This is not your model.
https://stripe.com/docs/api#subscription_object
As you can see, you can ask that object for the plan, and then the plan for the amount.
I am a bit confused. So I would have to ask for the plan first before I can retrieve the amount?
Brian
Hey Brian,
Where's your confusion?
A webhook is sending you some JSON data, so you have to poke around through it to get what you want out (the amount in this case).
Your error says you called amount on a Stripe Subscription which doesn't have an amount, a plan does.
NoMethodError (undefined method `amount' for #Stripe::Subscription:0x00005630b03da5b8)
Print out your event in the terminal or byebug / pry it so you can see what data you've got.
I guess I am just not understanding how to change it from a subscription to a plan.
I am also currently using a developement url not a production one for my webhooks.
irb(main):001:0> e = Stripe::Event.retrieve("evt_1D3oX72OobZT8tZuFcNxXcpk")
=> #<Stripe::Event:0x2b1cb2657800 id=evt_1D3oX72OobZT8tZuFcNxXcpk> JSON: {
"id": "evt_1D3oX72OobZT8tZuFcNxXcpk",
"object": "event", "api_version": "2018-05-21",
"created": 1535390985, "data": {"object":{"id":"sub_DUo9TmZzClFG1N","object":"subscription","application_fee_percent":null,"billing":"charge_automatically","billing_cycle_anchor":1535390983,"cancel_at_period_end":false,"canceled_at":null,"created":1535390983,"current_period_end":1538069383,"current_period_start":1535390983,"customer":"cus_DT7O4lor9yOHge","days_until_due":null,"discount":null,"ended_at":null,"items":{"object":"list","data":[{"id":"si_DUo92aCIKDATBh","object":"subscription_item","created":1535390984,"metadata":{},"plan":{"id":"monthly","object":"plan","active":true,"aggregate_usage":null,"amount":1900,"billing_scheme":"per_unit","created":1534660009,"currency":"cad","interval":"month","interval_count":1,"livemode":false,"metadata":{},"nickname":"Monthly","product":"prod_DRdeH2Q4y35faL","tiers":null,"tiers_mode":null,"transform_usage":null,"trial_period_days":null,"usage_type":"licensed"},"quantity":1,"subscription":"sub_DUo9TmZzClFG1N"}],"has_more":false,"total_count":1,"url":"/v1/subscription_items?subscription=sub_DUo9TmZzClFG1N"},"livemode":false,"metadata":{},"plan":{"id":"monthly","object":"plan","active":true,"aggregate_usage":null,"amount":1900,"billing_scheme":"per_unit","created":1534660009,"currency":"cad","interval":"month","interval_count":1,"livemode":false,"metadata":{},"nickname":"Monthly","product":"prod_DRdeH2Q4y35faL","tiers":null,"tiers_mode":null,"transform_usage":null,"trial_period_days":null,"usage_type":"licensed"},"quantity":1,"start":1535390983,"status":"active","tax_percent":null,"trial_end":null,"trial_start":null}},
"livemode": false,
"pending_webhooks": 1,
"request": {"id":"req_jreFxqxCqmXk5P","idempotency_key":null},
"type": "customer.subscription.created"
}
irb(main):002:0> Webhooks::ChargeSucceeded.new.call(e)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."stripe_id" = $1 LIMIT $2 [["stripe_id", "cus_DT7O4lor9yOHge"], ["LIMIT", 1]]
Charge Load (0.2ms) SELECT "charges".* FROM "charges" WHERE "charges"."user_id" = $1 AND "charges"."stripe_id" = $2 ORDER BY "charges"."id" ASC LIMIT $3 [["user_id", 9], ["stripe_id", "sub_DUo9TmZzClFG1N"], ["LIMIT", 1]]
(0.1ms) BEGIN
Charge Create (33.9ms) INSERT INTO "charges" ("user_id", "stripe_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["user_id", 9], ["stripe_id", "sub_DUo9TmZzClFG1N"], ["created_at", "2018-08-27 22:52:08.728957"], ["updated_at", "2018-08-27 22:52:08.728957"]]
(7.2ms) COMMIT
Traceback (most recent call last):
2: from (irb):2
1: from app/models/webhooks/charge_succeeded.rb:10:in `call'
NoMethodError (undefined method `amount' for #<Stripe::Subscription:0x00005639687c59c8>)
Inside of Stripe
Webhooks
Pending
http://localhost:3000/webhooks/stripe
Status
503 Pending (5 tries)
Retry history
[2018/08/27 11:29 to http://localhost:3000]: (503) ERR
[2018/08/27 12:49 to http://localhost:3000]: (503) ERR
[2018/08/27 14:02 to http://localhost:3000]: (503) ERR
[2018/08/27 15:07 to http://localhost:3000/webhooks/stripe]: (503) ERR
[2018/08/27 15:47 to http://localhost:3000/webhooks/stripe]: (503) ERR
Request
{
"id": "evt_1D3oX72OobZT8tZuFcNxXcpk",
"object": "event",
"api_version": "2018-05-21",
"created": 1535390985,
"data": {
"object": {
"id": "sub_DUo9TmZzClFG1N",
"object": "subscription",
"application_fee_percent": null,
"billing": "charge_automatically",
"billing_cycle_anchor": 1535390983,
"cancel_at_period_end": false,
"canceled_at": null,
"created": 1535390983,
"current_period_end": 1538069383,
"current_period_start": 1535390983,
"customer": "cus_DT7O4lor9yOHge",
"days_until_due": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_DUo92aCIKDATBh",
"object": "subscription_item",
"created": 1535390984,
"metadata": {
},
"plan": {
"id": "monthly",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1900,
"billing_scheme": "per_unit",
"created": 1534660009,
"currency": "cad",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"nickname": "Monthly",
"product": "prod_DRdeH2Q4y35faL",
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"subscription": "sub_DUo9TmZzClFG1N"
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_DUo9TmZzClFG1N"
},
"livemode": false,
"metadata": {
},
"plan": {
"id": "monthly",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1900,
"billing_scheme": "per_unit",
"created": 1534660009,
"currency": "cad",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
},
"nickname": "Monthly",
"product": "prod_DRdeH2Q4y35faL",
"tiers": null,
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"start": 1535390983,
"status": "active",
"tax_percent": null,
"trial_end": null,
"trial_start": null
}
},
"livemode": false,
"pending_webhooks": 1,
"request": {
"id": "req_jreFxqxCqmXk5P",
"idempotency_key": null
},
"type": "customer.subscription.created"
}
Response
host localhost:3000 resolves to illegal IP 127.0.0.1
PayPal and Stripe are two of the most popular and well-known payment processing platforms. Both companies have similarities, but each platform is better suited for different business needs. While both companies specialize in online payment processing (over in-person transactions),