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
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.
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
// 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!
Top comments (1)
Hello Galih.
Thank you for interesting article.
I don't know vue and maybe i just missed something.
But how can we protect against fake requests? For instance if i send this request manually from console.Where in controller we check that payment was successful ?