Help us understand the problem. What is going on with this article?

【Rails】pay.jpの導入(ビューのカスタム〜登録まで)

最初に

商品購入時等に必要なクレジットカードの登録機能をpayjpを用いて実装しました。
ビューのカスタム〜登録までの手順を備忘録として残します。

開発環境

・Rails 5.2.4.2
・Ruby 2.5.1

pay.jpとは

https://pay.jp/docs/started
簡単にいうと、クレジットカードの登録〜決済を代行してくれるサービスです。
payjpジェムを使用することによって機能の実装を助けてくれます。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_513632_19d75450-6adb-d809-1541-e37e81b1650f.png

ちなみにカード情報そのものを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」ページをクリックしてテスト秘密鍵とテスト公開鍵を確認します。
スクリーンショット 2020-06-09 19.12.21.png

・gemを追加

gem 'payjp'
gem 'dotenv-rails'

pay.jpとdotenvを使用できる様にします。
dotenvは環境変数をファイルで読み込める様にするgemです。
dotenv、後で環境変数を読み込まないといけない過程があるので先に追加しておきます。
追加後、bundle install。

install完了後、.envファイルをホームディレクトリに作成します。

スクリーンショット 2020-06-10 17.58.06.png
.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タグで読み込めるようにする。

application.html.haml
 !!!
%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を使用する方法もありますが、今回はビューをカスタマイズしたいので、割愛します。

スクリーンショット 2020-06-09 19.45.21.png
作成途中ではありますが、この様なviewになります。

app/view/devise/registrations/new_credit_card.html.haml
= 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を活用して実装します。

app/aseets/javascripts/payjp.js
$(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.モデル、テーブルの作成

20200602032423_create_credit_cards.rb
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に登録できる様にします。

app/models/credit_card.rb
class CreditCard < ApplicationRecord
  #アソシエーション
  belongs_to :user

  #バリデーション
  validates :user_id, :customer_id, :card_id, presence: true
end

4.ルーティングの作成。

routes.rb
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.コントローラーの修正。

app/controllers/users/registrations_contoroller.rb
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

Nosuke0808
プログラミング歴数ヶ月の者ですがよろしくお願いいたします! 日々のアウトプットと備忘録として投稿しており、これから就職活動に入ります。初学者ゆえに至らぬ点ばかりかと思いますが暖かく見守っていただければ幸いです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした