5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RailsでのSquare webクレジット決済のサンプル

Last updated at Posted at 2021-08-25

SquareWebPaymentsSDKを用いたカード決済サイトを構築する一般的な方法は以下のドキュメントに記載されています。

前提

  • 店舗でのキャッシュレス決済プラットフォームSquareのアカウントを持っている
  • Square APIについて知っている
  • サンプルプロジェクトを実行する前に、開発者向けダッシュボードで、APIアクセスにに必要なApplicationID, AccessTokenを発行しておく。ここでLocationIDも確認できます。
  • ruby on rails の基礎知識(railsコマンド、ディレクトリ構造、model,view,controllerの関係など)を有している。
  • https://github.com/yasunao/sq_rails_web_payment_exampleにソースコードを置いておきました。

画面のサンプル

new.png

追加した主な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関連のコミュニティに貢献できれば幸いです。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?