Easy Paypal Checkout in Rails using Smart Payment Buttons
Table of contents
- SETUP
- Get Paypal Client ID and Secret
- Integrate Paypal smart button to html views
- PROGRESSING
- Convert the button to vue component
- //cover_image: https://direct_url_to_image.jpg
- Getting the order creation to Paypal dynamic
- Setup the Paypal Capture endpoint to capture payment
- Finishing: Enabling submitting CSRF token
Before we start, don't forget to run these commands for your convenience:
bundle install
yarn
rake db:create
rake db:migrate
rake db:seed
SETUP
Get Paypal Client ID and Secret
- Get sandbox Client ID
- Create application.yml using figaro and use it to store the ID, so that you don't (accidentally) commit the id and secret to the public.
bundle exec figaro install
config/application.yml
development:
paypal_client_id: PUT_YOUR_PAYPAL_SANDBOX_CLIENT_ID_HERE
paypal_client_sec: PUT_YOUR_PAYPAL_SANDBOX_CLIENT_SECRET_HERE
test:
paypal_client_id: PUT_YOUR_PAYPAL_SANDBOX_CLIENT_ID_HERE
paypal_client_sec: PUT_YOUR_PAYPAL_SANDBOX_CLIENT_SECRET_HERE
PRO TIP: Please remember to RESTART your server ^_^
Integrate Paypal smart button to html views
- Add the paypal button container on the product item html partial, below the TODO: comment
app/views/store/products/_product.html.erb
<!-- TODO: Put the paypal button here -->
<div id="paypal-button-container"></div>
- Add script source from PAYPAL to the bottom of the body element, after the footer
app/views/layouts/store/application.html.erb
<script
id="paypal-sdk-js-src"
src="https://www.paypal.com/sdk/js?client-id=paypal_client_id"
data-sdk-integration-source="button-factory"
>
</script>
<script>
paypal.Buttons({
env: 'sandbox', // Valid values are sandbox and live.
createOrder: function(data, actions) {
// This function sets up the details of the transaction, including the amount and line item details.
return actions.order.create({
purchase_units: [{
amount: {
value: '0.01' // This will be replaced, I promise
}
}]
});
},
onApprove: async (data) => {}
}).render('#paypal-button-container');
</script>
- Reference the client id from application.yml app/views/layouts/store/application.html.erb
<--- REDACTED --->
<script
id="paypal-sdk-js-src"
src="https://www.paypal.com/sdk/js?client-id=<%= ENV['paypal_client_id'] %>"
data-sdk-integration-source="button-factory"
></script>
<--- REDACTED --->
After adding this, you can reload and you will see that the Paypal button shows up and working. You can even click it and start making (mock) payment!
If you click the Pay with paypal button:
And if you click the "Debit or Credit Card" button, it will render a form like this for free:
But we are not done, since completing this checkout will not trigger anything (even though it will be successful) and the checkout amount is mocked to USD0.01 at the moment. So the rest of the post, will be for getting our checkout working as normal, even production-ready.
Excited? Let's get on with it, then.
PROGRESSING
Convert the button to vue component
Remove the second script that we added on the layout template so that it will only look like this:
app/views/layouts/store/application.html.erb
</footer>
<script
id="paypal-sdk-js-src"
src="https://www.paypal.com/sdk/js?client-id=<%= ENV['paypal_client_id'] %>"
data-sdk-integration-source="button-factory"
>
</script>
</body>
Only keep the script source from Paypal, then we add the button container, by replacing the previous button html on the product item partial html
app/views/store/products/_product.html.erb
<!-- TODO: Put the paypal button here -->
<div class="buynow">
<paypal-button
refer="paypal-container-<%= product.id.to_s %>"
/>
</div>
Now if you refresh, the button will disappear, our mission for the next 5 mins is to make that re-appear
- Intro for the vue pack: store.js
This is the current state of the Vue pack that will be responsible to hold all the javascripts needed to easily get our Paypal button to be dynamic.
app/javascript/packs/store.js
import Vue from 'vue/dist/vue.esm'
document.addEventListener('DOMContentLoaded', () => {
var storeList = new Vue({
el: '', // Add the css identifier for the div that will load the paypal button
components: {
// Add paypal button here
}
})
})
This Vue pack is already hooked to our rails HTML template on the layout template for the storefront.
app/views/layouts/store/application.html.erb:13
%= javascript_pack_tag 'store' %>
- Add Paypal JS SDK using yarn
yarn add @paypal/sdk-client
then add to the store.js
import Vue from 'vue/dist/vue.esm'
// add this single line below here to use the Paypal JS SDK we've just installed
import { unpackSDKMeta } from '@paypal/sdk-client';
document.addEventListener('DOMContentLoaded', () => {
- Create the vue component: paypal button
app/javascript/components/paypal_button.vue
<template>
<div :id="refer"></div>
</template>
<script>
export default {
props: {
// This is for giving dynamic container css 'ID' to be called by the Paypal button renderer
refer: {
type: String,
required: true
}
},
data() {
},
mounted: function() {
},
computed: {
},
methods: {
}
}
</script>
The last thing for this step is to hook this newly-created component onto the store.js pack
import Vue from 'vue/dist/vue.esm'
import { unpackSDKMeta } from '@paypal/sdk-client';
// Add this single line below
import PaypalButton from '../components/paypal_button.vue';
document.addEventListener('DOMContentLoaded', () => {
var storeList = new Vue({
el: '#store-list', // Add the css identifier for the div that will load the paypal button
components: {
// Add paypal button here
PaypalButton
}
})
})
Notice that we have just added this line:
components: {
PaypalButton
}
and this line:
el: '#store-list',
but for this line to be working, we need to remember to add this element identifier onto the rails template also
app/views/store/products/index.html.erb
<div
class="section section-demo"
id="store-list" <!-- This is what we add -->
>
<div class="container">
<%= render partial: "product", collection: @products, as: :product %>
</div>
</div>
Two lines, that's it. We will not be adding anymore to this file.
So the setup is ready, all we need to do now is to get the component working.
- Setup the vue component: paypal button
app/javascript/components/paypal_button.vue
// REDACTED //
<script>
export default {
props: {
refer: {
type: String,
required: true
},
// This is where will pass more product attributes to be used in this vue component from the rails html template
},
data() {
return {
// Defaults template for the order JSON payload that will be sent to Paypal
order: {
description: "",
amount: {
currency_code: "",
value: 0
}
}
};
},
mounted: function() {
// IMPORTANT: this method causes the paypal button be loeaded and rendered
this.setLoaded();
},
computed: {
selectorContainer() {
return '#' + this.refer;
}
},
methods: {
setLoaded: function() {
paypal
.Buttons({
createOrder: (data, actions) => {
return actions.order.create({
purchase_units: [
this.order
]
});
},
onApprove: async (data, actions) => {
},
onError: err => {
console.log(err);
}
}).render(this.selectorContainer);
}
}
};
</script>
Up to this point, your component should be ok enough that it will re-appear and working once more.
title: End-to-end Paypal Checkout with Rails (Part 3 of 3) series: Easy Paypal Checkout in Rails published: false description: tags:
//cover_image: direct_url_to_image.jpg
If you want to code along with this post, you can do so by checking out from this commit on indiesell repo.
Getting the order creation to Paypal dynamic
First of all, we want to ask payment from our customers according to the product they choose to buy, right? So that is our first objective, and where we'll capitalize on our hardwork of turning the paypal button into Vue component.
We can easily pass the attributes from our products, that were created on the backend, to the front-end, which is our Paypal button:
From:
app/views/store/products/_product.html.erb
<!-- TODO: Put the paypal button here -->
<div class="buynow">
<paypal-button
refer="paypal-container-<%= product.id.to_s %>"
/>
</div>
To:
app/views/store/products/_product.html.erb
<!-- TODO: Put the paypal button here -->
<div class="buynow">
<paypal-button
refer="paypal-container-<%= product.id.to_s %>"
currency-code="<%= product.price_currency %>"
price-str="<%= product.price.to_s %>"
product-description="<%= product.name %>"
product-id="<%= product.id %>"
/>
</div>
Here we have added the currency, price, product description, and also the id of the product, so that it can be used in the component.
app/javascript/components/paypal_button.vue
export default {
props: {
refer: {
type: String,
required: true
},
// Pass the product attributes to be used here
currencyCode: {
type: String,
required: false,
default() {
return 'USD'
}
},
priceStr: {
type: String, // should be like "100.00"
required: true
},
productDescription: {
type: String,
required: true
},
productId: {
type: String,
required: true
}
},
// REDACTED
The data that we pass from the rails template as props, will override our default Paypal order payload to trigger checkout process using the smart payment buttons:
app/javascript/components/paypal_button.vue
// REDACTED
mounted: function() {
// These 3 lines are what we add here
this.order.description = this.productDescription;
this.order.amount.currency_code = this.currencyCode;
this.order.amount.value = Number(this.priceStr);
// IMPORTANT: to cause the paypal button be loeaded and rendered
this.setLoaded();
},
// REDACTED
Now if you refresh, when you click one of the payment buttons, you will see that the amount we charge our customers is dynamic, as per set for the product selected.
So by this point, we are able to ask payment from our customers correctly, but the any successful, valid payment, will still not trigger anything on our app. So let's change that on!
Setup the Paypal Capture endpoint to capture payment
First, because we want to also store the successful payments that our customers made on Paypal from the smart buttons, we need to record it as "Purchase" on our DB. And we can achieve just that by creating an endpoint to do just that, and hook it to the "onApprove" callback from the smart button.
So the implementation is up to you, but for indiesell, I implemented something like this:
app/controllers/api/v1/store/paypal_purchases_controller.rb
# frozen_string_literal: true
module API
module V1
module Store
class PaypalPurchasesController < ApplicationController
# We'll remove this line below, i promise to you
skip_before_action :verify_authenticity_token
def create
# TODO: this is where we put the magic
end
end
end
end
end
app/controllers/api/v1/store/paypal_purchases_controller.rb
def create
# TODO
purchase = Purchase.new
purchase.gateway_id = 1
purchase.gateway_customer_id = params[:customer_id]
purchase.customer_email = params[:customer_email]
purchase.product_id = params[:product_id]
purchase.token = params[:token]
purchase.is_paid = params[:is_successful]
# Because price_cents is string of "20.00", we need to
# parse the string to money. To do that we need to build the compatible money string,
# should be like "USD 20.00"
money_string = "#{params[:price_currency]} #{params[:price_cents]}"
parsed_money = Monetize.parse money_string
purchase.price_cents = parsed_money.fractional # 2000
purchase.price_currency = parsed_money.currency.iso_code # USD
if purchase.save
render status: :ok, json: { purchase_code: purchase.id }
else
render status: :unprocessable_entity, json: {}
end
end
So on the endpoint, we should be prepping the purchase record based on the payload that we receive from the "onApprove" callback on the paypal_button.vue.
After prepping, we then try to save it. If it is successful, then we declare status 200, if not then 422, as the json response.
Now that the endpoint is ready, let's hook it to the vue component to have an end to end process setup.
app/javascript/components/paypal_button.vue
methods: {
setLoaded: function() {
paypal
.Buttons({
// REDACTED
onApprove: async (data, actions) => {
const order = await actions.order.capture();
// for complete reference of order object: https://developer.paypal.com/docs/api/orders/v2
const response = await fetch('/api/v1/store/paypal_purchases', {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(
{
price_cents: this.priceStr,
price_currency: this.currencyCode,
product_id: this.productId,
token: order.orderID,
customer_id: order.payer.payer_id,
customer_email: order.payer.email_address,
is_successful: order.status === 'COMPLETED'
}
)
});
const responseData = await response.json();
if (response.status == 200) {
window.location.href = '/store/purchases/' + responseData.purchase_code + '/success';
} else {
window.location.href = '/store/purchases/failure?purchase_code=' + responseData.purchase_code;
}
},
onError: err => {
console.log(err);
}
}).render(this.selectorContainer);
}
}
I know it seems a lot, and I do apologize if this step is a bit overwhelming. But don't worry, we'll discuss it one by one.
- The receiving of the callback from paypal ```js onApprove: async (data, actions) => { const order = await actions.order.capture();
So the order constant is basically the "capture" result, meaning that when the customer that checks out using our Smart Payment buttons, Paypal knows to where the successful payment callback should be posted to, we just need to capture it and store it.
2. The acknowledgment of successful payment for our app
Now that Paypal knows our customer has successfully paid the bill, then we need to acknowledge it also, hence this action of sending POST request to the endpoint we created earlier
```js
// REDACTED
const response = await fetch('/api/v1/store/paypal_purchases', {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(
{
price_cents: this.priceStr,
price_currency: this.currencyCode,
product_id: this.productId,
token: order.orderID,
customer_id: order.payer.payer_id,
customer_email: order.payer.email_address,
is_successful: order.status === 'COMPLETED'
}
)
});
Take a good look on the JSON object with the :body key, that is essentially the payload that we will be processing on the endpoint that we made. So you can just customize, add, or remove any data as you see fit.
- Notify/Redirect user
const responseData = await response.json();
if (response.status == 200) {
window.location.href = '/store/purchases/' + responseData.purchase_code + '/success';
} else {
window.location.href = '/store/purchases/failure?purchase_code=' + responseData.purchase_code;
}
So again, this is entirely up to you, where or how you want to notify your customers that the payment, aside from being completed in Paypal, have also been acknowledged by your database.
In the case of Indiesell, I redirect the customers to success page if successful and failure page if there is something wrong on the endpoint. The successful and failure page have been made beforehand, so I will not cover that on this post.
Finishing: Enabling submitting CSRF token
So last but not least, remember about the promise I made to you on this post earlier? app/controllers/api/v1/store/paypal_purchases_controller.rb
# redacted
class PaypalPurchasesController < ApplicationController
skip_before_action :verify_authenticity_token
def create
# redacted
Yes, that bit. That bit actually is unsafe for production, since it bypasses one of security features from Rails. I skipped that bit just to keep things simpler to complete our checkout development, but now we're done, let's get on it then.
First, remove that unsafe line.
app/controllers/api/v1/store/paypal_purchases_controller.rb
# redacted
class PaypalPurchasesController < ApplicationController
def create
# redacted
Now with this, our checkout system will fail once more during the capture callback. What we need to do is to submit CSRF token created by rails for POST request that we send to our endpoint
So first we create a mixin to specifically fetch the CSRF token from the HTML: app/javascript/mixins/csrf_helper.js
var CsrfHelper = {
methods:{
findCsrfToken() {
let csrf_token_dom = document.querySelector('meta[name="csrf-token"]');
let csrf_token = "";
if (csrf_token_dom) {
csrf_token = csrf_token_dom.content;
}
return csrf_token;
}
}
};
export default CsrfHelper;
Then, we must not forget to import that mixin and declare it in our paypal_button.vue component
app/javascript/components/paypal_button.vue
<template>
<div :id="refer"></div>
</template>
<script>
// MIXINS
// For grabbing the CSRF token to be used to submit to internal API endpoint
import CsrfHelper from '../mixins/csrf_helper.js';
export default {
mixins:[CsrfHelper],
Once done, use it by calling it before we send the POST request:
app/javascript/components/paypal_button.vue
// REDACTED
const response = await fetch('/api/v1/store/paypal_purchases', {
method: 'POST',
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": this.findCsrfToken() // taken from the mixins/csrf_helper.js
},
body: JSON.stringify(
// REDACTED
And we're done. If you have been coding along, please refresh the page and try to complete a purchase.
Or if you want to check the source code for this series of posts, you can do so by checking out this branch on the indiesell repo.
Happy coding, cheers!