1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】PAY.JPについて

Last updated at Posted at 2025-07-02

記事概要

Ruby on RailsにPAY.JPを実装する方法について、まとめる

前提

  • Macを使用している
  • Ruby on Railsでアプリケーションを作成している
  • ライブラリ「pry-rails」をインストール済みである
  • PAY.JPのアカウントを作成済みである
  • PAY.JPが提供する「テストモード」を使用する

サンプルアプリ(GitHub)

PAY.JPとは

クレジットカード決済代行サービスの一つであり、多くの決済代行サービスの中でも、シンプルなAPIで開発をしやすいことが特徴。

処理の仕組み

PAY.JPとの間に発生する様々な決済情報の送受信は、PAY.JPがオープンAPIとして提供している仕組みを利用する。そのため、アプリケーション側で特別な対策をしなくても、セキュリティ的に担保された処理を行うことができる

Image from Gyazo

処理イメージ

トークンは一度だけ有効となるものなので、同じトークンで別の決済を行うことはできない。

Image from Gyazo

開発環境

PAY.JPが提供する「テストモード」を使用すると、実際のクレジットカード請求や、手数料の発生はない

payjp.js

クライアントサイドからPAY.JPのAPIを使用するために読み込むJavaScriptのライブラリ。この中に、安全にクレジットカード情報をトークン化するための処理などが含まれている

手順①(Turbo機能をオフ)

PAY.JPが存在するページに遷移するリンクに対して、data: { turbo: false }を追記

<%= link_to "購入画面に進む", data: { turbo: false },class:"item-red-btn"%>

手順②(クレジットカード情報のトークン化)

Image from Gyazo

  1. クライアントサイドでPAY.JPのAPIを使用するために必要なJavaScriptを読み込むため、app/views/layouts/application.html.erbのhead要素内に、payjp.jsのライブラリを記述する

    <!DOCTYPE html>
    <html>
      <head>
        <!-- 中略 -->
        
        <!-- payjp.jsのライブラリを追加 -->
        <script type="text/javascript" src="https://js.pay.jp/v2/pay.js"></script>
        
        <!-- 中略 -->
      </head>
    
      <body>
        <%= yield %>
      </body>
    </html>
    
  2. app/javascript配下に、実際にトークン化を行うJavaScriptファイルを作成する

  3. 上記で作成したファイルを読み込むための設定を行う

    config/importmap.rb
    # 最終行に追加
    pin "card", to: "card.js"
    
    app/javascript/application.js
    // 最終行に追加
    import "card"
    
  4. JavaScriptファイルが正しく読み込まれることを確認するため、下記を記述する

    card.js
    const pay = () => {
      console.log("カード情報トークン化のためのJavaScript");
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    
  5. ブラウザのコンソール上に、「カード情報トークン化のためのJavaScript」が表示されることを確認する
    Image from Gyazo

  6. フォーム送信時にイベントが発火するように、下記を記述する

    card.js
    const pay = () => {
      const form = document.getElementById('charge-form')
      form.addEventListener("submit", (e) => {
        console.log("フォーム送信時にイベント発火")
        e.preventDefault();
      });
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    

    Image from Gyazo

  7. PAY.JPにログイン後、ページの左側の「API」をクリックすると公開鍵を確認できる。「テスト公開鍵」をコピーし、公開鍵を取得する

  8. 公開鍵の情報を、JavaScriptのファイルに設定する
    ※鍵情報を公開しない。このコードの状態でGitHubにPushするなどしてしまうと、コードに含まれる鍵情報が公開されてしまい、不正請求などの被害にあうリスクがある

    card.js
    const pay = () => {
      // PAY.JPテスト公開鍵
      const payjp = Payjp('pk_test_***********************')
      const form = document.getElementById('charge-form')
      form.addEventListener("submit", (e) => {
        console.log("フォーム送信時にイベント発火")
        e.preventDefault();
      });
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    
  9. フォームに、クレジットカード情報の入力項目を作成する

    <%= form_with  model: @order, id: 'charge-form', class: 'card-form',local: true do |f| %>
      <%= render 'layouts/error_messages', model: @order %>
      <div class='form-wrap'>
        <%= f.label :price, "金額" %>
        <%= f.text_field :price, class:"price", placeholder:"例)2000" %>
      </div>
      
      <!-- クレジットカード情報入力欄 -->
      <div class='form-wrap'>
        <p>カード番号</p>
        <div id="number-form" class="input-default"></div>
      </div>
      <div class='form-wrap'>
        <p>期限</p>
        <div id="expiry-form" class="input-default"></div>
      </div>
      <div class='form-wrap'>
        <p>カード背面4桁もしくは3桁の番号</p>
        <div id="cvc-form" class="input-default"></div>
      </div>
      <%= f.submit "購入", class:"button", id: "button" %>
    <% end %>
    
  10. ブラウザで、クレジットカードの入力項目が表示されたことを確認する
    Image from Gyazo

  11. フォームに、クレジットカード情報の入力フォームを作成する

    card.js
    const pay = () => {
      // PAY.JPテスト公開鍵
      const payjp = Payjp('pk_test_***********************')
    
      // elementsインスタンスを作成
      const elements = payjp.elements();
    
      // クレジットカードの入力フォーム作成
      const numberElement = elements.create('cardNumber');
      const expiryElement = elements.create('cardExpiry');
      const cvcElement = elements.create('cardCvc');
    
      // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える
      numberElement.mount('#number-form');
      expiryElement.mount('#expiry-form');
      cvcElement.mount('#cvc-form');
    
      const form = document.getElementById('charge-form')
      form.addEventListener("submit", (e) => {
        console.log("フォーム送信時にイベント発火")
        e.preventDefault();
      });
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    

    elements.create()で指定できるタイプは下記。

    指定可能なtype 説明
    card カード番号入力欄、有効期限入力欄、CVC入力欄の順に横に並んだフォーム
    cardNumber カード番号入力欄
    cardExpiry 有効期限入力欄
    cardCvc CVC入力欄
  12. フォームに入力されたカードの情報を取得してトークン化するため、下記を記述する

    card.js
    const pay = () => {
      // PAY.JPテスト公開鍵
      const payjp = Payjp('pk_test_***********************')
    
      // elementsインスタンスを作成
      const elements = payjp.elements();
    
      // クレジットカードの入力フォーム作成
      const numberElement = elements.create('cardNumber');
      const expiryElement = elements.create('cardExpiry');
      const cvcElement = elements.create('cardCvc');
    
      // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える
      numberElement.mount('#number-form');
      expiryElement.mount('#expiry-form');
      cvcElement.mount('#cvc-form');
    
      const form = document.getElementById('charge-form')
      form.addEventListener("submit", (e) => {
        // カード情報のトークンを取得する
        payjp.createToken(numberElement).then(function (response) {
          if (response.error) {
          } else {
            const token = response.id;
            console.log(token)
          }
        });
        e.preventDefault();
      });
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    
  13. テストカードの情報を使用し、クレジットカード情報送信時にトークンが生成されることを、ブラウザで確認する
    Image from Gyazo

    テストカードの情報は下記。詳細は、こちらを参照

    項目
    テストカード番号 4242424242424242(16桁)
    CVC 123
    有効期限 登録時より未来

手順③(サーバーサイドへのトークン送付)

Image from Gyazo

  1. トークンの値をフォームに含めるため、下記を記述する
    card.js
    const pay = () => {
      // PAY.JPテスト公開鍵
      const payjp = Payjp('pk_test_***********************')
    
      // elementsインスタンスを作成
      const elements = payjp.elements();
    
      // クレジットカードの入力フォーム作成
      const numberElement = elements.create('cardNumber');
      const expiryElement = elements.create('cardExpiry');
      const cvcElement = elements.create('cardCvc');
    
      // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える
      numberElement.mount('#number-form');
      expiryElement.mount('#expiry-form');
      cvcElement.mount('#cvc-form');
    
      const form = document.getElementById('charge-form')
      form.addEventListener("submit", (e) => {
        // カード情報のトークンを取得する
        payjp.createToken(numberElement).then(function (response) {
          if (response.error) {
          } else {
            const token = response.id;
    
            // トークンの値をHTMLのinput要素に埋め込み、フォームに追加する
            const renderDom = document.getElementById("charge-form");
            const tokenObj = `<input value=${token} name='token'>`;
            renderDom.insertAdjacentHTML("beforeend", tokenObj);
            debugger; //ここで停止
          }
        });
        e.preventDefault();
      });
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    
  2. ブラウザ上にトークンの値が現れ、フォームの中に追加されたことを確認する
    Image from Gyazo
  3. トークンの値を非表示にするため、下記を記述する
    card.js
    const pay = () => {
      // PAY.JPテスト公開鍵
      const payjp = Payjp('pk_test_***********************')
      
      // elementsインスタンスを作成
      const elements = payjp.elements();
    
      // クレジットカードの入力フォーム作成
      const numberElement = elements.create('cardNumber');
      const expiryElement = elements.create('cardExpiry');
      const cvcElement = elements.create('cardCvc');
    
      // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える
      numberElement.mount('#number-form');
      expiryElement.mount('#expiry-form');
      cvcElement.mount('#cvc-form');
    
      const form = document.getElementById('charge-form')
      form.addEventListener("submit", (e) => {
        // カード情報のトークンを取得する
        payjp.createToken(numberElement).then(function (response) {
          if (response.error) {
          } else {
            const token = response.id;
    
            // トークンの値をHTMLのinput要素に埋め込み、フォームに追加する
            const renderDom = document.getElementById("charge-form");
            // トークンの値を非表示にする
            const tokenObj = `<input value=${token} name='token' type="hidden">`;
            renderDom.insertAdjacentHTML("beforeend", tokenObj);
          }
        });
        e.preventDefault();
      });
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    
  4. ブラウザにはトークンの値が表示されず、フォームにはトークンの値が追加されていることを確認する
    Image from Gyazo
  5. フォームに存在するクレジットカードの各情報を削除するため、下記を記述する
    card.js
    const pay = () => {
      // PAY.JPテスト公開鍵
      const payjp = Payjp('pk_test_***********************')
    
      // elementsインスタンスを作成
      const elements = payjp.elements();
    
      // クレジットカードの入力フォーム作成
      const numberElement = elements.create('cardNumber');
      const expiryElement = elements.create('cardExpiry');
      const cvcElement = elements.create('cardCvc');
    
      // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える
      numberElement.mount('#number-form');
      expiryElement.mount('#expiry-form');
      cvcElement.mount('#cvc-form');
    
      const form = document.getElementById('charge-form')
      form.addEventListener("submit", (e) => {
        // カード情報のトークンを取得する
        payjp.createToken(numberElement).then(function (response) {
          if (response.error) {
          } else {
            const token = response.id;
    
            // トークンの値をHTMLのinput要素に埋め込み、フォームに追加する
            const renderDom = document.getElementById("charge-form");
            // トークンの値を非表示にする
            const tokenObj = `<input value=${token} name='token' type="hidden">`;
            renderDom.insertAdjacentHTML("beforeend", tokenObj);
          }
          // フォームに存在するクレジットカードの各情報を削除する
          numberElement.clear();
          expiryElement.clear();
          cvcElement.clear();
        });
        e.preventDefault();
      });
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    
  6. サーバーサイドへフォームの情報を送信するため、下記を記述する
    card.js
    const pay = () => {
      // PAY.JPテスト公開鍵
      const payjp = Payjp('pk_test_***********************')
    
      // elementsインスタンスを作成
      const elements = payjp.elements();
    
      // クレジットカードの入力フォーム作成
      const numberElement = elements.create('cardNumber');
      const expiryElement = elements.create('cardExpiry');
      const cvcElement = elements.create('cardCvc');
    
      // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える
      numberElement.mount('#number-form');
      expiryElement.mount('#expiry-form');
      cvcElement.mount('#cvc-form');
    
      const form = document.getElementById('charge-form')
      form.addEventListener("submit", (e) => {
        // カード情報のトークンを取得する
        payjp.createToken(numberElement).then(function (response) {
          if (response.error) {
          } else {
            const token = response.id;
    
            // トークンの値をHTMLのinput要素に埋め込み、フォームに追加する
            const renderDom = document.getElementById("charge-form");
            // トークンの値を非表示にする
            const tokenObj = `<input value=${token} name='token' type="hidden">`;
            renderDom.insertAdjacentHTML("beforeend", tokenObj);
          }
          // フォームに存在するクレジットカードの各情報を削除する
          numberElement.clear();
          expiryElement.clear();
          cvcElement.clear();
          // サーバーサイドへフォームの情報を送信する
          document.getElementById("charge-form").submit();
        });
        // 通常のRuby on Railsにおけるフォーム送信処理はキャンセル
        e.preventDefault();
      });
    };
    
    window.addEventListener('turbo:load', pay);
    window.addEventListener("turbo:render", pay);
    

手順④(決済機能のサーバーサイド実装)

Image from Gyazo

  1. paramsを確認するため、binding.pryを追記する
    orders_controller.rb
    class OrdersController < ApplicationController
      def index
        @order = Order.new
      end
    
      def create
        binding.pry # ここで処理停止
        @order = Order.new(order_params)
        if @order.valid?
          @order.save
          return redirect_to root_path
        else
          render 'index', status: :unprocessable_entity
        end
      end
    
      private
      def order_params
        params.require(:order).permit(:price)
      end
    end
    
  2. ブラウザでフォームの送信を行い、paramsの中身を確認する
    [1] pry(#<OrdersController>)> params
    => #<ActionController::Parameters {"authenticity_token"=>"M8O8hcgWQ4zJBLuh9OWEs0ZMXrMjUC9BW9Irmq2JcGr2RVr7wPxbsfeEZQOCRTM05keVG60y_-tWcfYLspEVSA", "order"=>{"price"=>"2000"}, "token"=>"tok_867ae3a9e4d8280018fcdd3a849a", "controller"=>"orders", "action"=>"create"} permitted: false>
    
    # priceの情報はorderというキーのバリューとして、ハッシュ形式で含まれている
    [2] pry(#<OrdersController>)> order_params[:price]
    => "2000"
    
    # tokenの情報はorderというキーのバリューとしては含まれていない
    [3] pry(#<OrdersController>)> order_params[:token]
    => nil
    
  3. mergeメソッドを用いてtokenの値を結合するため、下記を記述する
    orders_controller.rb
    # 中略
    private
    def order_params
      # ストロングパラメーターに、tokenの値を結合
      params.require(:order).permit(:price).merge(token: params[:token])
    end
    
    # 中略
    
  4. ブラウザでフォームの再送信を行い、paramsの中身を確認する
    [1] pry(#<OrdersController>)> params
    => #<ActionController::Parameters {"authenticity_token"=>"cDAcCU6XcG05HwlMQ3-fNv1wsiMCGWXdKYzIw_1TmT-1tvp3Rn1oUAef1-413yixXXt5i4x7tXckLxVS4kv8HQ", "order"=>{"price"=>"1500"}, "token"=>"tok_6d460ccc66a0708edfcc71b0ddaf", "controller"=>"orders", "action"=>"create"} permitted: false>
    
    # priceの情報はorderというキーのバリューとして、ハッシュ形式で含まれている
    [2] pry(#<OrdersController>)> order_params[:price]
    => "1500"
    
    # tokenの情報もorderというキーのバリューとして、ハッシュ形式で含まれている
    [3] pry(#<OrdersController>)> order_params[:token]
    => "tok_6d460ccc66a0708edfcc71b0ddaf"
    
  5. binding.pryの記述を削除する
  6. 「Orderモデル(Orderクラス)」にtokenという属性が存在しないため、エラーが発生する
    Image from Gyazo
  7. Orderモデルでtokenの値を取り扱えるようにする
    order.rb
    class Order < ApplicationRecord
      attr_accessor :token # tokenの値をモデルで取り扱えるようにする
      validates :price, presence: true, numericality: {only_integer: true}
    end
    
  8. Gemのpayjpを導入する
    導入方法は、こちらを参照
  9. PAY.JPにログイン後、ページの左側の「API」をクリックすると秘密鍵を確認できる。「テスト秘密鍵」をコピーし、秘密鍵を取得する
  10. 秘密鍵の情報を、コントローラーのファイルに設定する
    ※鍵情報を公開しない。このコードの状態でGitHubにPushするなどしてしまうと、コードに含まれる鍵情報が公開されてしまい、不正請求などの被害にあうリスクがある
    orders_controller.rb
    # 中略
    
    def create
      @order = Order.new(order_params)
      if @order.valid?
        # バリデーションを正常に通過した時のみに、決済処理を実行
    
        # PAY.JPテスト秘密鍵
        Payjp.api_key = "sk_test_***********"
        Payjp::Charge.create(
          amount: order_params[:price],  # 決済金額
          card: order_params[:token],    # トークン化されたカード情報
          currency: 'jpy'                # 通貨の種類(日本円)
        )
        @order.save
        return redirect_to root_path
      else
        render 'index', status: :unprocessable_entity
      end
    end
    
    # 中略
    
  11. サーバーを再起動する
  12. ブラウザでフォームの再送信を行い、下記を確認する
    • 金額がordersテーブルに保存されるか
    • PAY.JP上で決済の記録が残るか
      PAY.JPにログインし、「売上」の項目を選択することで閲覧可能
  13. リファクタリングする
    orders_controller.rb
    class OrdersController < ApplicationController
      def index
        @order = Order.new
      end
      def create
        @order = Order.new(order_params)
        if @order.valid?
          # バリデーションを正常に通過した時のみに、決済処理を実行
          pay_item
          @order.save
          return redirect_to root_path
        else
          render 'index', status: :unprocessable_entity
        end
      end
    
      private
      def order_params
        # ストロングパラメーターに、tokenの値を結合
        params.require(:order).permit(:price).merge(token: params[:token])
      end
    
      # メソッドに決済処理を切り出す
      def pay_item
        # PAY.JPテスト秘密鍵
        Payjp.api_key = "sk_test_***********"
        Payjp::Charge.create(
          amount: order_params[:price],  # 決済金額
          card: order_params[:token],    # トークン化されたカード情報
          currency: 'jpy'                # 通貨の種類(日本円)
        )
      end
    end
    
  14. ブラウザでフォームの再送信を行い、下記を確認する
    • 金額がordersテーブルに保存されるか
    • PAY.JP上で決済の記録が残るか
      PAY.JPにログインし、「売上」の項目を選択することで閲覧可能
  15. tokenが空では保存できないというバリデーションを設定するため、下記を記述する
    本来tokenをOrderモデルのバリデーションとして記載できないが、attr_accessor :tokenを記述したことで、バリデーションを設定できる
    order.rb
    class Order < ApplicationRecord
      attr_accessor :token # tokenの値をモデルで取り扱えるようにする
      validates :price, presence: true, numericality: {only_integer: true}
      validates :token, presence: true # tokenが空では保存できない
    end
    
  16. クレジット情報が不足している状態で、ブラウザのフォーム送信を行い、正常にエラーが表示されることを確認する
    Image from Gyazo

ここまで実装してきた機能のイメージ

Image from Gyazo

手順⑤(環境変数の設定)

APIの鍵情報は外部に流出しないように工夫する必要がある

開発環境

鍵情報は環境変数として設定し、自身のPCだけに保持する

  1. ターミナルで以下のコマンドを実行し、環境変数を設定する
    ※現在の.zshrcの内容を削除せず、末尾に上記の設定を追記。既存の設定を削除してしまうとPCが正常に動作しなくなる危険性がある
    # zshrcファイルを開く
    % vim ~/.zshrc
    
    # iを押してインサートモードに移行し、下記を追記する。
    export PAYJP_SECRET_KEY='sk_test_*************'
    export PAYJP_PUBLIC_KEY='pk_test_*************'
    
    # 編集が終わったらescキーを押してから:wqと入力して保存して終了
    
  2. 下記コマンドを実行し、設定を反映する
    % source ~/.zshrc
    
  3. 下記コマンドで、コンソールを起動する
    % rails c
    
  4. コンソールで、環境変数が設定されていることを確認する
    [1] pry(main)> ENV["PAYJP_SECRET_KEY"]
    => "sk_test_*************"
    [2] pry(main)> ENV["PAYJP_PUBLIC_KEY"]
    => "pk_test_*************"
    
  5. サーバーサイドで環境変数を読み込むため、下記のように編集する
    orders_controller.rb
    # 中略
    
    def pay_item
      # PAY.JPテスト秘密鍵
      Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 直接秘密鍵を指定していたところを、環境変数に置き換え
      Payjp::Charge.create(
        amount: order_params[:price],  # 決済金額
        card: order_params[:token],    # トークン化されたカード情報
        currency: 'jpy'                # 通貨の種類(日本円)
      )
    end
    
    # 中略
    
  6. 「gon」というGemを導入する
    導入方法は、こちらを参照
    ※ブラウザの開発者ツールで内容を見ることができてしまうため、本来は秘密情報を扱うためのものではない。公開鍵は厳密には秘匿する必要がないため、今回はgonを利用する
  7. ビューで「gon」を使用できるようにするため、下記を追記する
    <!-- gonを使用するために追記 -->
    <%= include_gon %>
    
    <!-- 省略 -->
    
    include_gonは、必ず表示しようとするビューに記述する必要がある。ビューが1つしかない場合は問題にならないが、複数のビューで使用する場合は全ビューに記述を行うか、application.html.erbに記述する必要がある
  8. RailsからJavaScriptへ公開鍵を渡すため、下記を記述する
    orders_controller.rb
    # 中略
    
    def index
      gon.public_key = ENV["PAYJP_PUBLIC_KEY"] # RailsからJavaScriptへ公開鍵を渡す
      @order = Order.new
    end
    
    # 中略
    
    gon.(変数名) = (代入する値)と記述することで、JavaScriptで変数を使用可能
  9. JavaScriptで公開鍵を読み込む
    card.js
    const pay = () => {
      // gon.public_keyを変数publicKeyに代入
      const publicKey = gon.public_key
      // PAY.JPテスト公開鍵
      const payjp = Payjp(publicKey)
      
      // 中略
    
  10. サーバーを再起動する
  11. ブラウザで問題なく作動することを確認する

本番環境(Render)

はじめてデプロイするアプリに環境変数を設定

こちらを参照

デプロイ済みのアプリに環境変数を設定

  1. Renderにサインインし、ダッシュボードを開く
  2. 環境変数を設定したいアプリをクリックし、詳細ページを開く
  3. サイドバーにある「Environment」をクリックし、環境変数の設定欄を表示する
  4. Add Environment Variablesをクリックし、下記2つを追加する
    • PAYJP_PUBLIC_KEY:公開鍵
    • PAYJP_SECRET_KEY:秘密鍵
  5. Valueに各値を入力する
  6. Save Changesをクリックし、保存する
  7. サーバーを再起動する

手順⑥(不備があった場合の処理を修正)

現状

render後、クレジットカード情報は入力できない
Image from Gyazo

修正手順

  1. render時にgonを使用するための設定を行う
    orders_controller.rb
    # 中略
    
    def create
      @order = Order.new(order_params)
      if @order.valid?
        # バリデーションを正常に通過した時のみに、決済処理を実行
        pay_item
        @order.save
        return redirect_to root_path
      else
        gon.public_key = ENV["PAYJP_PUBLIC_KEY"] # render時の環境変数読み込み
        render 'index', status: :unprocessable_entity
      end
    end
    
    # 中略
    
  2. renderが実行された場合でもJavaScriptのコードが実行されるように記述する必要があるが、記述済み。
    card.js
    // 省略
    window.addEventListener("turbo:render", pay);
    

修正後

render後、クレジットカード情報は入力できる
Image from Gyazo

Ruby on Railsまとめ

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?