LoginSignup
2
3

More than 3 years have passed since last update.

ウィザード形式の新規登録を実装〜SMS認証を添えて〜

Last updated at Posted at 2020-02-16

はじめに

ウィザード形式は、登録フォームをいくつかのページに分けて実装するやり方です。入力項目がたくさんあるサービスの場合に有効です。
ウィザード形式でのフォーム実装は既に色々な方が記事にしており、実装方法も十人十色です。sessionで値を保持したり、ページ毎にDBに値を格納したり。。
やり方としては数多く存在しますが、今回の記事では、そんなウィザード形式の実装方法の一つをご紹介できればと思います。ウィザード形式の実装をするに当たって、参考になった記事は下記に掲載しています。細かな導入方法(deviseの導入方法やTwilioの登録方法など)については本記事では割愛いたしますので、リンク先の記事をご確認くださいませ。。

開発条件

  • Ruby 2.5.1
  • Rails 5.2.3
  • Haml・Sass記法
  • 'devise'のgemを使用
  • Twilioの登録(SMS認証実装のため)

実装概要

  • 1ページ目でuserテーブルに格納する値を入力してもらう
  • 2ページ目でuser_addressテーブルのtelephoneだけ入力してもらう(任意)
  • 3ページ目でSMS認証が可能(任意)
  • 4ページ目でuser_addressテーブルの残りのカラムを入力してもらう
  • 5ページ目で登録完了画面!

DB設計

ユーザ関係のDBは次のとおりです!
今回は2つのDBを用意して、それぞれに値を格納します。

usersテーブル

Column Type Options
nickname string null: false
email string null: false
password string null: false
password_confirmation string null: false
last_name string null: false
first_name string null: false
last_name_kana string null: false
first_name_kana string null: false
birthday date null: false

user_addressesテーブル

Column Type Options
user_id references null: false, foreign_key: true
last_name string null: false
first_name string null: false
last_name_kana string null: false
first_name_kana string null: false
postal_code integer null: false
prefecture integer null: false
city string null: false
address string null: false
building string
telephone integer

実装コード!

完成したコードがこちらです!

routes

ルート設定は以下のとおり。もう少しスマートに書きたい。。
HTTPのメソッドとしては、最初に呼び出されるuser_infoと最後に表示されるuser_completeはGETで、2,3,4ページはPOSTで通信を行なっています。

routes.rb
Rails.application.routes.draw do
 devise_for :users, controllers: {
    sessions: 'users/sessions',
    registrations: 'signup'
  } 

  resources "signup", only: [:index, :create], path: "/signup" do
    collection do
      get 'user_info' # 1ページ目
      post 'user_tel' # 2ページ目
      post 'user_tel_verification' # 3ページ目
      post 'user_address' # 4ページ目
      get 'user_complete' # 登録完了後のページ
    end
  end
end

models

モデルも2つ用意しました!
バリデーションはお好みで実装できます。

user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_one :user_address
  accepts_nested_attributes_for :user_address
end
user_address.rb
class UserAddress < ApplicationRecord
  belongs_to :user, inverse_of: :user_address

controller

before_actionが多くなってしまいましたが、取り敢えずはこんな感じに実装。
SMSに関する記述はコントローラーに書いてます。sessionはかなり苦戦した部分ではありますが、今では(気持ちの一方通行ではありますが)マブダチだと思っています!

signup.controller.rb
class SignupController < ApplicationController
  #before_action多すぎ??
  before_action :save_user_info_to_session, only: :user_tel
  before_action :save_user_tel_to_session, only: :user_tel_verification
  before_action :send_sms_code, only: :user_tel_verification
  before_action :check_sms_code, only: :user_address
  before_action :save_user_address_to_session, only: :create

  def user_info #1ページ目(userテーブル)
    @user = User.new
    @user.build_user_address
  end

  def save_user_info_to_session #1ページ目登録後に呼び出し、sessionに値を渡す
    session[:user_params] = user_params
    @user = User.new(session[:user_params]) # インスタンス作成時にsessionを引数として渡す
    render '/signup/user_info' unless @user.valid?
  end 

  def user_tel #2ページ目(user_addressテーブル)
    @user = User.new
    @user.build_user_address
  end

  def save_user_tel_to_session #2ページ目登録後に呼び出し、sessionに値を渡す
    session[:user_address_attributes_after_user_tel] = user_params[:user_address_attributes] # この時点では[:user_address_attributes]にはtelephoneしか格納されていない
    @user = User.new(session[:user_params]) # ここでもsessionを渡す
    @user.build_user_address(session[:user_address_attributes_after_user_tel]) # インスタンス作成時にsessionを引数として渡す
    render '/signup/user_tel' unless @user.valid?
  end

  def send_sms_code #2ページ目で登録した携帯番号にSMSを飛ばす記述
    @user = User.new
    @user.build_user_address
    send_phone_number = PhonyRails.normalize_number session[:user_address_attributes_after_user_tel][:telephone], country_code:'JP' #入力された携帯番号を変数に渡す
    session[:secure_code] = random_number_generator(4) #ランダムな4桁を生成するアクションに飛んだのち、sessionに渡す
    begin
      client = Twilio::REST::Client.new
      result = client.messages.create(
        from: ENV["TWILIO_PHONE_NUMBER"], #Twilioで取得した電話番号
        to:   send_phone_number, # 送り先
        body: "認証番号:#{session[:secure_code]}" # 送る内容
      )
    rescue Twilio::REST::RestError => e #エラー時の処理
      @messages = "エラーコード[#{e.code}] :”#{e.message}”"
      render '/signup/user_tel'
    end
  end

  def random_number_generator(n) #ランダムな4桁の番号を生成するアクション
    ''.tap { |s| n.times { s << rand(0..9).to_s } }
  end

  def user_tel_verification #3ページ目(SMSで送ったコードの確認ページ)
    @user = User.new
    @user.build_user_address
  end

  def check_sms_code #3ページ目で入力された認証コードを照らし合わせる
    session[:input_code] = user_params[:user_address_attributes] #入力されたコードをsessionに渡す
    @user = User.new
    @user.build_user_address
    redirect_to action: :user_tel_verification unless session[:input_code].present? #値が入力されているか確認
    render '/signup/user_tel_verification' and return unless session[:secure_code] == session[:input_code][:telephone] #SMSで送ったコードと実際に入力されたコードの照合
  end

  def user_address #4ページ目(user_addressテーブル)
    @user = User.new
    @user.build_user_address
  end

  def save_user_address_to_session #4ページ目登録後に呼び出し、sessionに値を渡す
    session[:user_address_attributes_after_user_address] = user_params[:user_address_attributes]
    session[:user_address_attributes_after_user_address].merge!(session[:user_address_attributes_after_user_tel]) # 4ページ目に登録したアドレスと2ページ目に登録した電話番号を結合
    @user = User.new
    @user.build_user_address
    render '/signup/user_address' unless session[:user_address_attributes_after_user_address].present? #本来はここでバリデーションしたいが、上手く行かないため止むを得ずpresent?
  end

  def create #4ページ目までの登録が完了した時点で、一気にDBに突っ込む
    @user = User.new(session[:user_params])
    @user.build_user_address(session[:user_address_attributes_after_user_address])
    if @user.save
      sign_in User.find(@user.id) #ログイン状態を保持する記述
      redirect_to user_complete_signup_index_path #登録完了画面にリダイレクト
    else
      render user_address_signup_index_path # ダメだった場合4ページ目にリダイレクト
    end
  end

  def user_complete #登録完了ページにて、sessionの削除(明示的に消さないとずっと残ってしまうため)
    session[:user_params].clear
    session[:secure_code].clear
    session[:input_code].clear
    session[:user_address_attributes_after_user_address].clear
  end

  private

  def user_params #ストロングパラメーターでユーザに関わる情報を保護
    params.require(:user).permit(
      :nickname, 
      :email, 
      :password, 
      :password_confirmation, 
      :last_name, 
      :first_name, 
      :last_name_kana, 
      :first_name_kana, 
      :birthday, 
      user_address_attributes: [:id, :last_name, :first_name, :last_name_kana, :first_name_kana, :postal_code, :prefecture, :city, :address, :building, :telephone]
    )
  end
end

views

全てのページの実装コードを記述してしまうと量が膨大になってしまうので省略。
form_forは非推奨なので、form_withに書き換えたほうが良いですね。。
そうなると、書き方としては、= form_with model: @user, url: ○○_path, local: true do |f|となると思います。

user_address.html.haml
.signup-panel
  %p.signup-panel__title 住所入力
  = form_for @user, url: ○○○○_path, method: :post, html: {class: "inputForm"} do |f|
    = f.fields_for :user_address do |p| #user_addressテーブルと紐付ける
      .signup-form
        .field
          = p.label "お名前"
          %span.form-require
          = p.text_field :last_name, class: "field__input", placeholder: "例)山田"
          = p.text_field :first_name, class: "field__input", placeholder: "例)太郎"
# ------------------------------------<中略>----------------------------------------------------
      .signup-btn
        = f.submit "次へ進む"

ちなみに。。
SMSで送った認証コードを入力してもらうフォームですが、labeltext_field:input_codeと指定することはできません。なので、取り敢えずは下記のように:telephoneを指定してあげるとエラーは起きません。DBには保存せず、sessionに渡してあげるだけなので、このような記載でも問題ないと思います!

user_tel_verification.html.haml
.signup-panel
    %p.signup-panel__title
      電話番号認証
    = form_for @user, url: ○○○○_path, method: :post, html: {class: "inputForm"} do |f|
      = f.fields_for :user_address do |p|
        .signup-form
          .field
            = p.label :telephone, "認証番号"
            %span.form-require
            = p.text_field :telephone, placeholder: "認証番号を入力"

終わりに

今回、復習をかねての記事投稿でしたが、アウトプットするとやはり知識の定着度は違いますね!取り敢えずは動くようになりましたが、まだまだリファクタリングが必要な部分もあるかと思いますし、これからも勉強継続は必要だなと思います。文章書くのは苦手ですが、アウトプットもなんとか継続していきたいです笑

参考

ウィザード形式関係

ウィザードフォーム形式フォームの実装 -devise×session×データを複数テーブルに分けて保存 ver.- (rails)
[Rails]ウィザードフォーム実装でfields_forを使って複数のテーブルに保存する

SMS認証関係

【Rails】Twilioを使ってSMS認証を実装してみる(めも)

2
3
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
2
3