最初に
商品購入時等に必要なクレジットカードの登録機能をpayjpを用いて実装しました。
ビューのカスタム〜登録までの手順を備忘録として残します。
開発環境
・Rails 5.2.4.2
・Ruby 2.5.1
pay.jpとは
https://pay.jp/docs/started
簡単にいうと、クレジットカードの登録〜決済を代行してくれるサービスです。
payjpジェムを使用することによって機能の実装を助けてくれます。
ちなみにカード情報そのものをDBに保存することは禁止されています。
payjpに保管されている情報を顧客idやカードidで呼び出すことで情報取得や支払いなどに対応する仕組みになっています。
要はpayjpが作ったcardとcustomerの情報とひもずいたidを渡すから、そのidをDBに保存してpayjpから参照してくれ。というイメージだと思います。セキュリティ面においてDBにクレジットカード情報が保存されるのは非常に危険ですのでこの様な形になっているのだと思います。(下記URL参照)
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738
##機能実装の前に
###・PAY.JPのアカウントを取得後、APIキーを確認。
https://pay.jp/
登録をしてログインした後、「API」ページをクリックしてテスト秘密鍵とテスト公開鍵を確認します。
###・gemを追加
gem 'payjp'
gem 'dotenv-rails'
pay.jpとdotenvを使用できる様にします。
dotenvは環境変数をファイルで読み込める様にするgemです。
dotenv、後で環境変数を読み込まないといけない過程があるので先に追加しておきます。
追加後、bundle install。
install完了後、.envファイルをホームディレクトリに作成します。
.envファイルに先ほど確認した秘密鍵と公開鍵を記入します。PAYJP_PRIVATE_KEY ='sk_test_****************:'
PAYJP_KEY ='pk_test_************************'
payjpのキー等、大事な情報が漏洩すると危険なのでgitには上がらない様に,.gitgnoreファイルに下記記述を追加します。
/.env
なお環境変数に直接コードを記入する方法もあります。
https://qiita.com/daisukeshimizu/items/c01f29f8398cc7f5c396
###viewのapplication.html.hamlの修正。
%script{src: "https://js.pay.jp/", type: "text/javascript"}
この一文を追加して、payjp.jsをheadタグで読み込めるようにする。
!!!
%html
%head
%meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/
%title EcApp
%script{src: "https://js.pay.jp/", type: "text/javascript"}
= csrf_meta_tags
= csp_meta_tag
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
%body
= yield
payjp.jsはカード情報のトークン化のみに特化したライブラリです。
payjp.jsライブラリは、 https://js.pay.jp にホストされているので、このドメインを読み込んで使用するため、ローカル上では動作しません。
これを読み込む事で、payjpからトークン取得などのメソッドを使用できるようになります。
##実装
###1.viewの作成
<script type="text/javascript" src="https://checkout.pay.jp" class="payjp-button" data-key="公開鍵"></script>
scriptタグを1行追加して、payjpがあらかじめ用意したviewを使用する方法もありますが、今回はビューをカスタマイズしたいので、割愛します。
作成途中ではありますが、この様なviewになります。= render partial: "items/header"
.main
%h2 新規会員登録
= form_with url: creditcards_path, method: :post, id: 'charge-form', html: { name: "inputForm" } do |f|
.main__form
.main__form__registration
クレジットカード登録ページ
.main__form__field
%label.main__form__field__label カード番号
= f.text_field :number, name: "number", id: "card_number", type: "text", placeholder: '半角数字のみ', class: 'main__form__field__box', maxlength: "16"
=image_tag "material/credit/credit.png", class:"main__form__field__credit-image"
.main__form__field2
%label.main__form__field__label 有効期限
= f.select :exp_month, [["--","--"],["01",1],["02",02],["03",03],["04",04],["05",05],["06",6],["07",07],["08",8],["09",9],["10",10],["11",11],["12",12]],{}, class: 'credit-box', name: "exp_month", id:"exp_month"
%span.month 月
= f.select :exp_year, [["--","--"],["2019",2019],["2020",2020],["2021",2021],["2022",2022],["2023",2023],["2024",2024],["2025",2025],["2026",2026],["2027",2027],["2028",2028],["2029",2029]], {}, class: 'credit-box', name: "exp_year", id:"exp_year"
%span.year 年
.main__form__field
%label.main__form__field__label セキュリティーコード
= f.text_field :cvc, name: "cvc", id:"cvc", type: "text", placeholder: 'カード背面4桁もしくは3桁の番号', class: "main__form__field__box", maxlength: "4"
.main__form__field
= f.submit '登録する', id: "token_submit", class: "main__form__field__submit-btn"
この後にpayjp.jsでフォームで受け取った値をトークンデータとして[payjpToken]に代入して、payjpにデータを送るので、
formと送信ボタン以外のform要素にそれぞれidとname属性を指定することが必須となります。
###2.payjpに送るトークンデータをJavascriptで作成
jQueryを使用するので、未設定の場合は設定をしてください。
https://qiita.com/ngron/items/95846bd630a723e00038
こちらの記事が参考になるかと思います。
jQueryを使用できる様になったら、
payjp.jsファイルにて、payjpに送るトークンデータを作成し、そのトークンをキーとしてクレジットカード情報などを登録する流れになります。
クレジットカード情報を入力してもらった後、その情報を元に”トークン”を作成するために、Javascriptを活用して実装します。
$(document).on('turbolinks:load', function() {
var form = $("#charge-form"); //id”charge-form”のものをformに代入します。
Payjp.setPublicKey('pk_test_839895c840d4f91f7e75df7e'); //公開鍵を直書き、して参照できる様にします。
$(document).on("click", "#token_submit", function(e) { //eが押されたときに作動します。
e.preventDefault(); //まずrailsの処理を止めて、jsの処理を先に行います。
form.find("input[type=submit]").prop("disabled", true);
var card = { //card変数に、入力されたクレジットカード情報をidを元に取得して、card変数に代入します。。
number: $("#card_number").val(),
cvc: $("#cvc").val(),
exp_month: $("#exp_month").val(),
exp_year: $("#exp_year").val(),
};
Payjp.createToken(card, function(s, response) { // トークンを生成。先ほどのcard情報がトークンという暗号化したものとして返ってくる
if (response.error) { //値がエラーであった場合
alert('カード情報が正しくありません');
}
else { //エラーが出なかった場合
$("#card_number").removeAttr("name");
$("#cvc").removeAttr("name");
$("#exp_month").removeAttr("name");
$("#exp_year").removeAttr("name"); //DBに保存しないため値を削除。
var token = response.id;
alert("登録が完了しました");
form.append($('<input type="hidden" name="payjpToken"/>').val(token)); //dbにトークンを保存するのでformにjsで作ったトークンを挿入している。
form.get(0).submit(); //formの先ほど挿入したデータをgetsしています。
}
});
});
});
https://payjp.hatenablog.com/entry/2017/12/05/134933
pay.jpが提供しているpayjp.jsのサンプルを参照してアレンジしています。
Payjp.setPublicKey('pk_test_xxxxxxxxxxxx');
これを記述しないとPayjpサーバーと通信が行われずトークンが発行されないので、先程確認したAPIキーを記入します。
※まだエラーが起きた時にリロードをしないとボタンが押せない仕様となっているため、今後修正したものを追加する予定です。
###3.モデル、テーブルの作成
class CreateCards < ActiveRecord::Migration[5.2]
def change
create_table :cards do |t|
t.references :user, foreign_key: true, null: false
t.string :customer_id, null: false
t.string :card_id, null: false
t.timestamps
end
end
end
credit_cardモデルを作成、PAY.JPの情報を保存するためのテーブルです。
・user_id ... Userテーブルのid
・customer_id ... payjpの顧客id
・card_id ... payjpのデフォルトカードのid
payjpから送られてくるカラムに対応する名前にして、DBに登録できる様にします。
class CreditCard < ApplicationRecord
#アソシエーション
belongs_to :user
#バリデーション
validates :user_id, :customer_id, :card_id, presence: true
end
###4.ルーティングの作成。
Rails.application.routes.draw do
#deviseのルーティング
devise_for :users, controllers: {
registrations: 'users/registrations'
}
devise_scope :user do
post 'users/sign_up', to: 'users/registrations#create'
get 'addresses', to: 'users/registrations#new_address'
post 'addresses', to: 'users/registrations#create_address'
get 'creditcards', to: 'users/registrations#new_credit_card'
post 'creditcards', to: 'users/registrations#pay'
end
新規登録と同じ流れで登録したいので、私はregistrationsコントローラーにまとめる様にしました。
credit_cardコントローラーを作成する場合は記述が変わるのでご注意ください。
###5.コントローラーの修正。
class Users::RegistrationsController < Devise::RegistrationsController
def create_address
@user = User.new(session["devise.regist_data"]["user"])
@address = Address.new(address_params)
unless @address.valid?
flash.now[:alert] = @address.errors.full_messages
render :new_address and return
end
@user.addresses.build(@address.attributes)
@user.save
session["devise.regist_data"]["user"].clear
sign_in(:user, @user)
redirect_to creditcards_path #クレジットカード登録のnewアクションへ飛ばす
end
def new_credit_card
card = CreditCard.where(user_id: current_user.id)
#クレジットカードのuser_idカラムへログイン中のユーザーのidが入ったハッシュを,cardに代入。
end
require "payjp" #APIキーを取得できる様に許可。
def pay
Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"] #payjpから送られてくる値を取得するために、秘密鍵で認証しています。
if params['payjpToken'].blank?
render :new_credit_card #JSで作成したpayjpTokenがからの場合、やり直し。
else
customer = Payjp::Customer.create(card: params['payjpToken'],) #customer変数に取得した値を代入しています。
@creditcard = CreditCard.new(user_id: current_user.id, customer_id: customer.id, card_id: customer.default_card)
if @creditcard.save
redirect_to root_path
else
redirect_to action: "new_credit_card"
end
end
end
pay.jpからの値取得には公式の記事がわかりやすかったため、参考にしました。
https://pay.jp/docs/api/?ruby#payjp-api
ウィザード形式でアドレスを登録した後に、クレジットカード登録にredirect_toさせているので、create_addressメソッドから記入しています。
deviseを用いた登録機能でウィザード形式での実装も記事に載せていますので、参考になれば幸いです。
https://qiita.com/Nosuke0808/items/00a8cac860abd68e2688
余談になりますが、実装をしてみてウィザード形式での実装ではrenderを使用していますがredirect_toで画面を切り替えていく方が良いかと思いました。
現状、addressの登録をしている際にページをリロードすると、ルーティングエラーとなってしまいます。
ユーザーの見え方としてはrenderでもredirect_toでも変わらなず、リロードしてエラーになってしまっては使い物にならないので、1つ1つ登録を完了した段階でDBに保存して次ページにリダイレクトさせるのが良いかと感じました。
今後renderを使用してリロードしてもエラーにならない方法を探すが、redirect_toに書き換える予定です。
以上でpayjpを用いたクレジッカードの登録までは以上になります。
実装をして,jsで作成したデータがpay.jpに送れず、pay.jpからのデータ取得もうまくいかず、実装にかなり時間がかかってしまいました。
理解できれば難しいことはないので、これからpayjpの実装に入る方に少しでも参考になれば幸いです。
##参考文献
https://techtechmedia.com/payjp-rails/
https://pay.jp/docs/started
http://payjp-announce.hatenablog.com/entry/2017/11/10/182738
https://qiita.com/daisukeshimizu/items/c01f29f8398cc7f5c396
https://payjp.hatenablog.com/entry/2017/12/05/134933
https://pay.jp/docs/api/?ruby#payjp-api