[Railsアプリ、pay.jp、firebase]決済機能付き駐車場貸し出しアプリを自作した

こんにちは、ヒカルです。

今回は私の所有する空いた土地を駐車場として貸し出すアプリを自作した経緯や苦労した点などを記します!



本記事の内容

  • 他のサイトは手数料で50%も取られるから自作したほうがお得!
  • [その場利用、予約機能]重複を防ぐためのvalidate
  • [Pay.jp]導入に苦労した決済システム
  • [firebase]なぜユーザー登録でdeviseではなくfirebaseにしたか

4つのテーマで紹介します。

まずは私が作成したアプリをご覧ください。

SHIMAHIKA-PARKING

他のサイトは手数料で50%も取られるから自作したほうがお得!

これまではakippaというサイトで駐車場として貸し出していました。

このサイトは簡単に貸し出し可能な駐車場にすることができるので非常に便利です。

ですが手数料で50%も取られてしまいます。

500円で貸し出していましたが、結局250円にしかならずそこまで利益を上げることができません。

なので私は自作することにしました。それと決済システムの導入の仕方について知りたかったというのもあります。

サーバーはherokuを使っていて、かかる経費は

hobbyモードなので700円
手数料は3~4%
です。



akippaで500円で貸し出した場合と自作アプリで400円で貸し出した時を比べると

6日/月 貸し出すとする

akippa: 500 × 6 × 0.5 = 1,500円
自作アプリ: 400 × 3.5% × 6 – 700 = 1,616円

6日間貸し出した時点で得になることがわかります。

需要が高い場所は自作がお勧めできますね。

[その場利用、予約機能]重複を防ぐためのvalidate


予約をする時に他の予約と重複してはいけないので以下のような形で実装しました。

app/models/reserve_validator.rb

class ReserveValidator < ActiveModel::EachValidator
    def validate_each(record, attribute, value)
        # 新規登録する期間
        new_start_date = record.start_on
        new_finish_date   = record.finish_on

        return unless new_start_date.present? && new_finish_date.present?

        # 重複する期間を検索(編集時は自期間を除いて検索)
        if record.id.present?
            not_own_reserves = Reserve.where('id NOT IN (?) AND start_on <= ? AND finish_on >= ?', record.id, new_finish_date, new_start_date)
            not_own_reserves_by_park = Park.where('id NOT IN (?) AND start_on <= ? AND finish_on_schedule >= ?', record.id, new_finish_date, new_start_date)
        else
            not_own_reserves = Reserve.where('start_on <= ? AND finish_on >= ?', new_finish_date, new_start_date)
            not_own_reserves_by_park = Park.where('start_on <= ? AND finish_on_schedule >= ?', new_finish_date, new_start_date)
        end
        record.errors.add(attribute, 'に重複があります') if not_own_reserves.present? or not_own_reserves_by_park.present?        
    end
end
config/locals/models/ja.yml
ja:
  activerecord:
    models:
      reserve: 予約
    attributes:
      reserve:
        start_on: 利用開始時刻
        finish_on: 利用終了時刻
      park:
        start_on: 利用開始時刻
        finish_on_schedule: 利用終了予定時刻
app/models/reserve.rb
class Reserve < ApplicationRecord
    validates :start_on, reserve: true
    validates :finish_on, reserve: true
end

attributeは設定しないと英語表記になってしまうので日本語表記になるように設定しましょう。

[Pay.jp]導入に苦労した決済システム

記事がhaml記法を使ったものがほとんどで僕はhaml記法を知らなかったものですから理解するのに苦労しました。

あとは記事通りに書いてもerrorが起こるものだから、1つ1つ理解してどこでerrorが起こってるのか解読するのに苦労しました。

誰かの参考になるようにコードを載せておきます。

私が参考にした記事も合わせて載せます。
Payjpでクレジットカード登録と削除機能を実装する(Rails)
[Rails]Pay.jpを利用したクレジット決済機能実装 ① ~実装の準備・APIの導入~

app/views/layouts/application.html.erb
<script type="text/javascript" src="https://js.pay.jp/"></script>
app/views/card/new.html.erb
<%=form_tag(pay_card_index_path, method: :post ,id:'charge-form', name: "inputForm") do %>
                                <div class="number-details">
                                    <div class="card-number">
                                        カード番号<span class="must-check">必須</span>
                                    </div>
                                </div>
                                <div class="number">
                                    <%= text_field_tag :number, "", class: "number", placeholder: "半角数字のみ" ,maxlength: "16", type: "text", id: "card_number" %>
                                </div>
                                <div class="brand-image">
                                    <img src="/cards/logo_visa.gif" alt="各種クレジットカードブランドロゴ" class="brand-logo">
                                    <img src="/cards/logo_mastercard.gif" alt="各種クレジットカードブランドロゴ" class="brand-logo">
                                    <img src="/cards/jcb.png" alt="各種クレジットカードブランドロゴ" class="brand-logo">
                                    <img src="/cards/american_express.png" alt="各種クレジットカードブランドロゴ" class="brand-logo">
                                    <img src="/cards/discover.png" alt="各種クレジットカードブランドロゴ" class="brand-logo">
                                    <img src="/cards/diners_club.png" alt="各種クレジットカードブランドロゴ" class="brand-logo">
                                </div>        
                                <div class="expirationdate">
                                    <div class="expirationdate-details">
                                        <div class="date">
                                            有効期限<span class="must-check">必須</span>
                                        </div>
                                    </div>
                                    <div class="expirationdate-choice d-flex">
                                        <div class="month d-flex">
                                            <%= select_tag "exp_month" ,options_for_select([['--', ''], ['01', '1'], ['02', '2'], ['03', '3'], ['04', '4'], ['05', '5'], ['06', '6'], ['07', '7'], ['08', '8'], ['09', '9'], ['10', '10'], ['11', '11'], ['12', '12']]) , type: "text"%>
                                            <div class="month-detail">月</div>
                                        </div>
                                        <div class="year d-flex">
                                            <%= select_tag "exp_year" ,options_for_select([['--', ''], ['21', '2021'], ['22', '2022'], ['23', '2023'], ['24', '2024'], ['25', '2025'], ['26', '2026'], ['27', '2027'], ['28', '2028'], ['29', '2029'], ['30', '2030'], ['31', '2031'], ['32', '2032']]), type: "text" %>
                                            <div class="year-detail">年</div>
                                        </div>
                                    </div>
                                </div>              
                                <div class="securitycode">
                                    <div class="securitycode-details">
                                        <div class="securitycode-title">
                                            セキュリティコード<span class="must-check">必須</span>
                                        </div>
                                    </div>
                                    <div class="securitycode-cardsecurity">
                                        <%= text_field_tag :cvc , "", class: "cvc", placeholder: "カード背面4桁もしくは3桁の番号", maxlength: "4", id: "cvc"%>
                                    </div>
                                </div>
                                <div class="submit">
                                    <%= submit_tag "登録する" , id: "token_submit" %>
                                </div>                                                
                            <% end %>
app/views/card/show.html.erb
<div class="col-xs-7 fs left">   
                            <div class="card-info">
                                <div class="card-info-brand">
                                    <img src="/cards/<%=@card_src%>" alt="クレジットカードブランド情報">
                                </div>
                                <div class="card-info-numbers">
                                    <div class="number">
                                        <%="**** **** **** " + @default_card_information.last4%>
                                    </div>
                                    <div class="expiration-date">
                                        <div class="expiration-date-title">
                                            有効期限
                                        </div>
                                        <div class="expiration-date-info">
                                            <%=@exp_month + " / " + @exp_year%>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>        
                        <div class="col-xs-5 fs right">
                            <%= link_to("削除する",delete_card_index_path, method: :post, class: "destroy__function__link__btn") %>
                        </div> 
app/javascript/pay.js
document.addEventListener(
  "DOMContentLoaded", e => {
    if (document.getElementById("token_submit") != null) { //token_submitというidがnullの場合、下記コードを実行しない
      Payjp.setPublicKey('pk_test_e22308e13375d08934d38e8f'); //ここに公開鍵を直書き
      let btn = document.getElementById("token_submit"); //IDがtoken_submitの場合に取得されます
      btn.addEventListener("click", e => { //ボタンが押されたときに作動します
        e.preventDefault(); //ボタンを一旦無効化します
        let card = {
          number: document.getElementById("card_number").value,
          cvc: document.getElementById("cvc").value,
          exp_month: document.getElementById("exp_month").value,
          exp_year: document.getElementById("exp_year").value
        }; //入力されたデータを取得します。
        Payjp.createToken(card, (status, response) => {
          if (status === 200) { //成功した場合
            $("#card_number").removeAttr("name");
            $("#cvc").removeAttr("name");
            $("#exp_month").removeAttr("name");
            $("#exp_year").removeAttr("name"); //データを自サーバにpostしないように削除
            $("#charge-form").append(
              $('<input type="hidden" name="payjp-token">').val(response.id)
            ); //取得したトークンを送信できる状態にします
            document.inputForm.submit();
            // alert("登録が完了しました"); //確認用            
          } else {
            alert("カード情報が正しくありません。"); //確認用
          }
        });
      });
    } 
  },
  false
);
app/controllers/purchase_controller.rb
class PurchaseController < ApplicationController
  require 'payjp'
  before_action :authenticate_user!

  def index
    @user = @current_user    
    if @parking = Park.find_by(user_id: @user.id, finish_stamp: "no")  
      if @parking.finish_on
        @card = Card.where(user_id: @user.id).first
        if @card.present?
          #Cardテーブルは前回記事で作成、テーブルからpayjpの顧客IDを検索                          
          Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
          #保管した顧客IDでpayjpから情報取得
          customer = Payjp::Customer.retrieve(@card.customer_id)
          #保管したカードIDでpayjpから情報取得、カード情報表示のためインスタンス変数に代入
          @default_card_information = customer.cards.retrieve(@card.card_id)  
          ##カードのアイコン表示のための定義づけ
          @card_brand = @default_card_information.brand
          case @card_brand
          when "Visa"
            # 例えば、Pay.jpからとってきたカード情報の、ブランドが"Visa"だった場合は返り値として
            # (画像として登録されている)Visa.pngを返す
            @card_src = "logo_visa.gif"
          when "JCB"
            @card_src = "jcb.png"
          when "MasterCard"
            @card_src = "logo_mastercard.gif"
          when "American Express"
            @card_src = "american_express.png"
          when "Diners Club"
            @card_src = "diners_club.png"
          when "Discover"
            @card_src = "discover.png"
          end
          # viewの記述を簡略化
          ## 有効期限'月'を定義
          @exp_month = @default_card_information.exp_month.to_s
          ## 有効期限'年'を定義
          @exp_year = @default_card_information.exp_year.to_s.slice(2,3)
        end   
      else
        redirect_to("")
        flash[:alert] = "利用を終了するを押してください"
      end
    else
      redirect_to("/purchase/history")
    end
  end

  def pay
    @user = @current_user
    @path = Rails.application.routes.recognize_path(request.referer)
    Payjp.api_key = ENV['PAYJP_PRIVATE_KEY']  
    if @path[:controller] == "purchase"
      @parking = Park.find_by(user_id: @user.id, finish_stamp: "no")
      @price = @parking.price
      @card = Card.find_by(user_id: @user.id)                   
      if @card.present?
        # ログインユーザーがクレジットカード登録済みの場合の処理
        # ログインユーザーのクレジットカード情報を引っ張ってきます。
        #登録したカードでの、クレジットカード決済処理
        charge = Payjp::Charge.create(
        # 商品(product)の値段を引っ張ってきて決済金額(amount)に入れる
        amount: @price,
        customer: Payjp::Customer.retrieve(@card.customer_id),
        currency: 'jpy'
        )
      else
        # ログインユーザーがクレジットカード登録されていない場合(Checkout機能による処理を行います)
        # APIの「Checkout」ライブラリによる決済処理の記述
        Payjp::Charge.create(
        amount: @price,
        card: params['payjp-token'], # フォームを送信すると作成・送信されてくるトークン
        currency: 'jpy'      
        )
      end
      @purchase = Purchase.create(user_id: @user.id, no_reservation_id: @user.id, start_on: @parking.start_on, finish_on: @parking.finish_on, price: @price)
      @parking.finish_stamp = "yes"
      @parking.save
      redirect_to action: 'done'  
  end

  def done
    @user = @current_user    
    @parking = Park.where(user_id: @user.id, finish_stamp: "yes").last
    @price = @parking.price
  end
end
app/controllers/card_controller.rb
class CardController < ApplicationController
  require 'payjp'  
  before_action :authenticate_user!

  def new
    @card = Card.where(user_id: @current_user.id)
    redirect_to("/user/#{@current_user.id}") if @card.exists?
  end

  def pay #payjpとCardのデータベース作成を実施します。
    Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
    if params['payjp-token']
      customer = Payjp::Customer.create(
      description: '登録テスト', #なくてもOK
      email: @current_user.email, #なくてもOK
      card: params['payjp-token'],
      metadata: {user_id: @current_user.id}
      ) #念の為metadataにuser_idを入れましたがなくてもOK
      @card = Card.new(user_id: @current_user.id, customer_id: customer.id, card_id: customer.default_card)
      if @card.save        
        redirect_to("/user/#{@current_user.id}")
      else
        redirect_to action: "pay"
      end
    else
      redirect_to action: "new", alert: "クレジットカードを登録できませんでした。"
    end
  end

  def delete #PayjpとCardデータベースを削除します
    card = Card.where(user_id: @current_user.id).first
    if card.blank?
      redirect_to action: "new"
    else
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      customer = Payjp::Customer.retrieve(card.customer_id)
      customer.delete
      card.delete
      if card.destroy
        redirect_to("/")
        flash[:notice] = "削除しました。"
      else
        redirect_to user_path, alert: "削除できませんでした。"
      end
    end
      
  end

  def show #Cardのデータpayjpに送り情報を取り出します
    card = Card.where(user_id: @current_user.id).first
    if card.blank?
      redirect_to action: "new" 
    else
      Payjp.api_key = ENV["PAYJP_PRIVATE_KEY"]
      customer = Payjp::Customer.retrieve(card.customer_id)
      @default_card_information = customer.cards.retrieve(card.card_id)

      ##カードのアイコン表示のための定義づけ
      @card_brand = @default_card_information.brand
      case @card_brand
      when "Visa"
        # 例えば、Pay.jpからとってきたカード情報の、ブランドが"Visa"だった場合は返り値として
        # (画像として登録されている)Visa.pngを返す
        @card_src = "logo_visa.gif"
      when "JCB"
        @card_src = "jcb.png"
      when "MasterCard"
        @card_src = "logo_mastercard.gif"
      when "American Express"
        @card_src = "american_express.png"
      when "Diners Club"
        @card_src = "diners_club.png"
      when "Discover"
        @card_src = "discover.png"
      end

      #  viewの記述を簡略化
      ## 有効期限'月'を定義
      @exp_month = @default_card_information.exp_month.to_s
      ## 有効期限'年'を定義
      @exp_year = @default_card_information.exp_year.to_s.slice(2,3)
    end
  end
end

このあとにも書いてあるとおり私はdeviseからfirebaseログインに変えたため、
authenticate_user!
はdeviseのものではなく独自に設定したものです。

したがって@current_userになっています。
お気をつけください。

[firebase]なぜユーザー登録でdeviseではなくfirebaseにしたか

最初はもちろんdeviseを使ってユーザー管理をしていました。
ですが途中から友達がIOSアプリとしても作りたいと言い出しました。

ユーザーを一致させたかったため、話し合いでfirebaseのauthenticateを使ったユーザー登録をすることにしました。
IOSはfirebaseしか使わないからと言われたからです。

ですが結論、railsでfirebaseログインを実装する必要はなかったと思います。

このアプリで言うと、その場利用や予約の情報もIOSに共有したいのでデータをバックエンドからswiftに送ることになります。



その手順でユーザーデータも送ることはできるはずなのでfirebaseログインを実装したのは経験としては価値がありましたが、無駄な労力でした。実装の説明が載ってる記事も片手で数えられるほどしかなかったからかなり苦労しましたし。

ただ、一応無事実装することができたので下にコードを載せておきます。

Googleログインだけ実装しています。

app/views/layouts/application.html.erb
<!-- The core Firebase JS SDK is always required and must be listed first -->
    <script src="https://www.gstatic.com/firebasejs/8.2.7/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.2.7/firebase-auth.js"></script>
    <!-- TODO: Add SDKs for Firebase products that you want to use
        https://firebase.google.com/docs/web/setup#available-libraries -->
    <script src="https://www.gstatic.com/firebasejs/8.2.7/firebase-analytics.js"></script>

    <script src="https://www.gstatic.com/firebasejs/ui/4.6.1/firebase-ui-auth.js"></script>
    <link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/4.6.1/firebase-ui-auth.css" />
    
    <script>
      // Your web app's Firebase configuration
      // For Firebase JS SDK v7.20.0 and later, measurementId is optional
      
      if(!firebase.apps.length) {
        const firebaseConfig = {
          apiKey: "xxxxxxxxx",
          authDomain: "xxxxxxxxx.firebaseapp.com",
          projectId: "xxxxxxxxx",
          storageBucket: "xxxxxxxxx.appspot.com",
          messagingSenderId: "xxxxxxxxx",
          appId: "xxxxxxxxx",
          measurementId: "xxxxxxxxx"
        };
        firebase.initializeApp(firebaseConfig);
        firebase.analytics();
      }
    </script>
    
  </body>
app/javascript/firebase.js
document.addEventListener("turbolinks:load" 
, function () {
$(function() {
    if (document.getElementById("firebaseui-auth-container") != null) { 
    var ui = new firebaseui.auth.AuthUI(firebase.auth());
    var uiConfig = {
        callbacks: {            
            signInSuccessWithAuthResult: (authResult, redirectUrl) => {
              authResult.user.getIdToken(true)
                .then((idToken) => { railsLogin(authResult.additionalUserInfo.isNewUser, idToken) })
                .catch((error)  => { console.log(`Firebase getIdToken failed!: ${error.message}`) });
              return false; // firebase側にログイン後はリダイレクトせず、railsへajaxでリクエストを送る
            },
            uiShown: () => { document.getElementById('loader').style.display = 'none' }
          },
          signInFlow: 'redirect',
          signInOptions: [
            firebase.auth.GoogleAuthProvider.PROVIDER_ID // Google認証
          ],
          tosUrl: '',
          // Privacy policy url/callback.
          privacyPolicyUrl: function() {
            window.location.assign('');
          }
        };
       // ログイン画面表示
        ui.start('#firebaseui-auth-container', uiConfig);

      
      var csrfTokenObj = () => {
        return { "X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content') };
      }
      
      var authorizationObj = (idToken) => {
        return { "Authorization": `Bearer ${idToken}` };
      }
      
      var railsLogin = (isNewUser, idToken) => {
        var url = isNewUser ? "/accounts" : "/login/googlecreate";        
        var headers = Object.assign(csrfTokenObj(), authorizationObj(idToken));
        $.ajax({url: url, type: "POST", headers: headers})
          .done((data) => { console.log("Rails login!")      })
          .fail((data) => { console.log("Rails login failed!") });
      }
    }
});
})
app/controllers/accounts_controller.erb
class AccountsController < FirebaseController
    def create
        super do |decoded_token|
          User.create(
            email: decoded_token['decoded_token'][:payload]['email'],
            uid:   decoded_token['uid']
          )
        end
      end
end
app/controllers/firebase_sessions_controller.erb
class FirebaseSessionsController < FirebaseController
    def create
        super do |decoded_token|
          User.find_by(uid: decoded_token['uid'])
        end
      end
    
      # DELETE /logout
      def destroy
        flash[:success] = 'ログアウトしました。'
        super
      end
end
app/controllers/firebase_controller.erb
class FirebaseController < ApplicationController
    def create
        if decoded_token = authenticate_firebase_id_token
          user = yield(decoded_token)
          session[:user_id] = user.id
          flash[:notice] = 'ログインしました。'
          redirect_to("/")
        else
          flash[:alert] = 'ログインできませんでした。'
          redirect_to("/login")
        end
      end
end
config/firebase_config.yml
 project_info:
    project_number: "xxxxxx"
    firebase_url: "https://xxxxxxx.firebaseio.com"
    project_id: "xxxxxxxx"
    secret: ""
Gemfile
gem 'jwt'
app/controllers/application_controller.erb
class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception        
    include SessionsHelper  

    private                                                        
    # tokenが正規のものであれば、デコード結果を返す
    # そうでなければfalseを返す
    def authenticate_firebase_id_token
      # authenticate_with_http_tokenは、HTTPリクエストヘッダーに
      # Authorizationが含まれていればブロックを評価する。
      # 含まれていなければnilを返す。
      authenticate_with_http_token do |token, options|
        begin
          decoded_token = FirebaseHelper::Auth.verify_id_token(token)
        rescue => e
          logger.error(e.message)
          false
        end
      end
    end

    
end
app/helpers/firebase_helper.rb
require 'jwt'
require 'yaml'
require 'net/http'

module FirebaseHelper
  CONFIG = YAML.load_file(Rails.root.join("config/firebase_config.yml"))

  module Auth

    ALGORITHM       = 'RS256'
    ISSUER_BASE_URL = 'https://securetoken.google.com/'
    CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'

    class << self
      def verify_id_token(token)
        raise 'Id token must be a String' unless token.is_a?(String)

        full_decoded_token = decode_jwt(token)

        errors = validate(full_decoded_token)
        raise errors.join(" / ") unless errors.empty?

        public_key = fetch_public_keys[full_decoded_token[:header]['kid']]
        unless public_key
          raise <<-EOS.squish
            Firebase ID token has "kid" claim which does not correspond to a known public key.
            Most likely the ID token is expired, so get a fresh token from your client app and try again.
          EOS
        end

        certificate = OpenSSL::X509::Certificate.new(public_key)
        decoded_token = decode_jwt(token, certificate.public_key, true, { algorithm: ALGORITHM, verify_iat: true })

        {
          'uid' => decoded_token[:payload]['sub'],
          'decoded_token' => decoded_token
        }
      end

      private
        def decode_jwt(token, key=nil, verify=false, options={})
          begin
            decoded_token = JWT.decode(token, key, verify, options)
          rescue JWT::ExpiredSignature => e
            raise 'Firebase ID token has expired. Get a fresh token from your client app and try again.'
          rescue => e
            raise "Firebase ID token has invalid signature. #{e.message}"
          end

          {
            payload: decoded_token[0],
            header: decoded_token[1]
          }
        end

        def fetch_public_keys
          uri = URI.parse(CLIENT_CERT_URL)
          https = Net::HTTP.new(uri.host, uri.port)
          https.use_ssl = true

          res = https.start {
            https.get(uri.request_uri)
          }
          data = JSON.parse(res.body)
          if (data['error']) then
            msg = %Q(Error fetching public keys for Google certs: #{data['error']} (#{res['error_description']})) if (data['error_description'])
            raise msg
          end

          data
        end

        def validate(json)
          errors     = Array.new
          project_id = FirebaseHelper::CONFIG['project_info']['project_id']
          payload    = json[:payload]
          header     = json[:header]
          issuer     = ISSUER_BASE_URL + project_id

          unless header['kid']                then errors << %Q(Firebase ID token has no "kid" claim.) end
          unless header['alg']  == ALGORITHM  then errors << %Q(Firebase ID token has incorrect algorithm. Expected "#{ALGORITHM}" but got "#{header['alg']}".) end
          unless payload['aud'] == project_id then errors << %Q(Firebase ID token has incorrect aud (audience) claim. Expected "#{project_id}" but got "#{payload['aud']}".) end
          unless payload['iss'] == issuer     then errors << %Q(Firebase ID token has incorrect "iss" (issuer) claim. Expected "#{issuer}" but got "#{payload['iss']}".) end
          unless payload['sub'].is_a?(String) then errors << %Q(Firebase ID token has no "sub" (subject) claim.) end
          if     payload['sub'].empty?        then errors << %Q(Firebase ID token has an empty string "sub" (subject) claim.) end
          if     payload['sub'].size > 128    then errors << %Q(Firebase ID token has "sub" (subject) claim longer than 128 characters.) end

          errors
        end
    end
  end
end

気をつけるポイントは

accounts_controllerとfirebase_controllerで
1行目

class AccountsController < FirebaseController

とすることです。

初めてコードをブログに載せましたがこんなに大変な作業だと思いませんでした。

これからありがたみを感じながら記事を読むことにします。

コメントを残す