SquareWebPaymentsSDKを用いたカード決済サイトを構築する一般的な方法は以下のドキュメントに記載されています。
- Web Payments SDK Overview
- Web Payments SDK Documentation: javascriptベースの決済フォーム作成に関するドキュメント
- Square API Technical Reference
- square/web-payments-quickstart : 今回のサンプルプロジェクトはこれをベースに作成しました。
- API Explorerが秀悦です。これで引数試しながらコードを書き上げるのが良いと思います。
前提
- 店舗でのキャッシュレス決済プラットフォームSquareのアカウントを持っている
- Square APIについて知っている
- サンプルプロジェクトを実行する前に、開発者向けダッシュボードで、APIアクセスにに必要なApplicationID, AccessTokenを発行しておく。ここでLocationIDも確認できます。
- ruby on rails の基礎知識(railsコマンド、ディレクトリ構造、model,view,controllerの関係など)を有している。
- https://github.com/yasunao/sq_rails_web_payment_exampleにソースコードを置いておきました。
画面のサンプル
追加した主なGems
GEM | 目的 |
---|---|
square.rb | システムにSquarePaymentsを組み込むのに必要。Catalog, Customers, Employees, Inventory, Labor, Locationを含むAPIアクセスが可能になります。 |
dotenv-rails | .envファイルにAPIアクセスにに必要なApplicationID, AccessToken、LocationIDを記録しておきます。詳しくは初心者向け!gem 'dotenv-rails'の使い方を参照。 |
Controller、Viewの作成
$ rails g controller patients index new create show
Controllerのコードのポイント
- get_client: SquareAPIへのコールは、全てこのClientオブジェクトを通して発行します。APIコールの詳細はhttps://github.com/square/square-ruby-sdkを参照して学習してください。
- list_paymet: サイトの決済履歴を得ます。シンプルですね。
- get_payment: 決済の結果を得ます。やはりシンプルです
- create_payment: 実際に決済を作成します。引数のnonce, priceは、newアクションのviewのフォームより得たパラメータです。square/web-payments-quickstartのように、Viewで決済を完了する実装モデルもあるのでしょうが、今回は、コントローラ上で決済プロセスを実行しました。
class PaymentsController < ApplicationController
# GET /payments or /payments.json
def index
@notice=params[:notice]
@payments = list_payment
end
# GET /payments/1 or /payments/1.json
def show
@payment = params[:payment].nil? ? get_payment(params[:id]).body.payment : params[:payment]
@notice=params[:notice]
end
# GET /payments/new
def new
@notice=params[:notice]
@price=params[:price]
end
# POST /payments or /payments.json
def create
@payment=create_payment(params[:nonce],params[:price].to_i)
respond_to do |format|
if @payment.success?
format.html { redirect_to payment_path(@payment.body.payment[:id], payment: @payment.body.payment, notice: "Payment was successfully created." )}
#format.json { render :show, status: :created, location: @payment }
else
format.html { redirect_to action: :new, 'data-turbolinks': false,price: params[:price],notice: "Payment was unprocessable" }
#format.json { render json: @payment.errors, status: :unprocessable_entity }
end
end
end
private
def get_square_client
access_token=ENV['SQUARE_ACCESS_TOKEN']
location_id=ENV['LOCATION_ID']
case Rails.env
when"production"
environment="production"
else
environment='sandbox'
end
client = Square::Client.new(
access_token: access_token,
environment: environment
)
return client
end
def list_payment
client=self.get_square_client
payments=client.payments.list_payments(
sort_order: "DESC",
location_id: ENV['LOCATION_ID']
)
payments= payments.body.try(:payments).nil? ? [] :payments.body.payments
return payments #upto 100 paymetns will return
end
def create_payment(nonce, price)
client=self.get_square_client
location_id=ENV['LOCATION_ID']
result = client.payments.create_payment(
body: {
source_id: nonce,
idempotency_key: SecureRandom.uuid(),
amount_money: {
amount: price,
currency: "JPY"
},
location_id: location_id
}
)
return result
end
def get_payment(id)
client=self.get_square_client
result = client.payments.get_payment(
payment_id: id
)
end
end
Viewのポイント
Web Payments SDKを用いたカード決済において特筆すべきviewはnew.html.erbの以下の3行のみ
<div id="card-container"></div>
<div id="payment-status-container"></div>
<%= f.hidden_field "card-nonce", name: "nonce" %>
後述するapp/javascripts/payments.jsで、card-containerタグ内にiframeのクレジットカード番号の入力フォームを表示します。Bootstrapベースになっています。ここで表示されているテキストボックスはすべてSquare側のiframe内で提供されており、Webサービス提供側からは触れないようになっています。ユーザからの見た目はWebサービス側そのままですし、デザインの自由度も高いのですがセキュリティ的に安全な仕組みになっています。
カード番号を入力してカード番号を決済ボタンを押すと、まずカード番号や有効期限、CVVといった情報がSquareのサーバに送られます。その結果、一時的に決済に使えるトークン(nonce)が発行されます。このトークンと決済金額を合わせて決済ができますが、これは一度しか使えません。(以上の情報は引用しました。クレジットカード番号を保有せずクレジットカード番号を保存するCard on Fileの使い方)
決済ボタンを押した時に、一時的に決済に使えるトークン(nonce)をhidden_fieldに格納し、createアクションにパラメータを送信するわけです。
## new.html.erb
<%case Rails.env%>
<%when 'production'%>
<%= javascript_include_tag "https://web.squarecdn.com/v1/square.js" , 'data-turbolinks-track': 'reload' %>
<%when 'development'%>
<%= javascript_include_tag "https://sandbox.web.squarecdn.com/v1/square.js" , 'data-turbolinks-track': 'reload' %>
<%end%>
<div class="container">
<h1>New Payment</h1>
<p id="notice"><%= @notice %></p>
<div class="col-md-8 order-md-1">
<%= form_with(id: "payment-form",url: payments_path, method: :post, local: true) do |f| %>
<div class="mb-3">
<label for="card" class="form-label">Credit Card</label>
<div id="card-container"></div>
<div id="payment-status-container"></div>
<%= f.hidden_field "card-nonce", name: "nonce" %>
</div>
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<%= f.text_field "price",class: "form-control"%>
</div>
<%= f.submit "Pay" ,id: "card-button",class: "btn btn-primary"%>
<% end %>
</div>
<%= link_to 'Back', payments_path %>
</div>
app/javascripts/payments.js
ほぼ、square/web-payments-quickstartをコピーしたものです。強いて言えば以下の3点が特徴でしょうか
- APIアクセスに必要なパラメータをprocess.env.APPLICATION_IDのように環境変数より取得している。herokuにデプロイする場合は、Herokuで環境変数を設定する方法を参考にしましょう。
- document.getElementById('card-nonce').value = token
- document.getElementById('payment-form').submit()
##app/javascripts/payments.js
const appId = process.env.APPLICATION_ID;
const locationId = process.env.LOCATION_ID;
async function initializeCard(payments) {
const card = await payments.card();
await card.attach('#card-container');
return card;
}
async function createPayment(token) {
const body = JSON.stringify({
locationId,
sourceId: token,
});
const paymentResponse = await fetch('/payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body,
});
if (paymentResponse.ok) {
return paymentResponse.json();
}
const errorBody = await paymentResponse.text();
throw new Error(errorBody);
}
async function tokenize(paymentMethod) {
const tokenResult = await paymentMethod.tokenize();
if (tokenResult.status === 'OK') {
return tokenResult.token;
} else {
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors
)}`;
}
//throw new Error(errorMessage);
return errorMessage
}
}
document.addEventListener('DOMContentLoaded', async function () {
if (!window.Square) {
return;
}
let payments;
try {
payments = window.Square.payments(appId, locationId);
} catch(e) {
const statusContainer = document.getElementById(
'payment-status-container'
);
statusContainer.className = 'missing-credentials';
statusContainer.style.visibility = 'visible';
console.error(e);
return;
}
let card;
try {
card = await initializeCard(payments);
} catch (e) {
console.error('Initializing Card failed', e);
return;
}
// Checkpoint 2.
async function handlePaymentMethodSubmission(event, paymentMethod) {
event.preventDefault();
try {
// disable the submit button as we await tokenization and make a payment request.
cardButton.disabled = true;
const token = await tokenize(paymentMethod);
// nonceの値をhiddenの中に入れます
document.getElementById('card-nonce').value = token;
} catch (e) {
console.error(e.message);
cardButton.disabled = false;
}
}
const cardButton = document.getElementById('card-button');
cardButton.addEventListener('click', async function (event) {
await handlePaymentMethodSubmission(event, card);
// 本来のフォームを送信します
document.getElementById('payment-form').submit();
});
});
考察
以前はちょっとしたWebサービスに決済機能を盛り込むのはセキュリティの面でハードルが高いと感じていました。しかしSquareAPIおよびSquareWebPaymentSDKが使えることで、容易に決済ロジックを組み込むことができるようになりました。
謝辞
Qiitaコミュニティにおけるエンジニアリングに関する知識を記録・共有のおかげで、これまでたくさんの問題を解決することができました。少しばかりですが、Ruby,Rials, SquareAPI関連のコミュニティに貢献できれば幸いです。