diveseのconfirmationでメールアドレスのみで仮登録を実装

「フット・イン・ザ・ドア」の心理を考えると、私は会員登録の時、最初にメールアドレスだけでメールを確認し、メールのリンクを踏んだ本登録時にパスワードやその他の情報を登録させた方が会員登録の確率が上がると思っています。

そこでrailsのdeviseで最初の入力をメールアドレスの入力だけで済まし、本登録時にパスワードを登録するようにカスタマイズしてみました。

バージョンは下記です。
rails (5.1.4)
devise (4.3.0)


下記のページが参考になりました。
https://github.com/plataformatec/devise/wiki/How-To:-Email-only-sign-up

rails初アプリなのでお作法とか色々まずいところあると思いますのでご指摘いただければと思います。

## コントローラーとViewを生成する

コントローラーやビューをカスタマイズできるよう作成します。

```
rails g devise:views
rails g devise:controllers users
```

コントローラーは今回userモデルなのでusers配下に作ります。詳しくは後述しますが、コントローラーはルートを書かないと作っただけではデフォルトのコントローラが採用されカスタマイズできないので注意してください。

## 新規登録フォームからパスワードを削除

パスワードとパスワード確認の

```diff:app/views/divise/registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true %>
  </div>

-  <div class="field">
-    <%= f.label :password %>
-    <% if @minimum_password_length %>
-    <em>(<%= @minimum_password_length %> characters minimum)</em>
-    <% end %><br />
-    <%= f.password_field :password, autocomplete: "off" %>
-  </div>
-
-  <div class="field">
-    <%= f.label :password_confirmation %><br />
-    <%= f.password_field :password_confirmation, autocomplete: "off" %>
-  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>
```

## 新規登録時にパスワードが空でも送信できるようにする

ただ、ビューからフォームを消しただけではバリデータに引っかかって送信できないので、それをどうにかします。

```rb:app/models/user.rb
class User < ApplicationRecord
  ...

  def password_required?
    super && confirmed?
  end
end

```

## 新規登録時にログインしてしまうのを防止

デフォルトの挙動はサインアップ時にログインしてしまいますが、今回の流れだとまだログインするのはおかしいのでそれを阻止します。

```rb:app/models/user.rb
class User < ApplicationRecord
  ...

  def active_for_authentication?
    super && confirmed?
  end

  def inactive_message
    confirmed? ? super : :needs_confirmation
  end
end
```

`active_for_authentication?`はユーザーの状態によって、ログインを阻止するメソッドです。`inactive_message`はその際のフラッシュメッセージのキーを返します。キーは頭に`signed_up_but_`が付与され`I18n`のキーに使用されます。

フラッシュメッセージが正しく表示されるように文言を登録します。

```yml:config/locales/devise.ja.yml
ja:
  devise:
    registrations:
      signed_up_but_needs_confirmation: "ご登録いただいたメールアドレスにメールを送信しました。メールを確認してアカウントを有効化してください。"
```

これで、確認用のメールアドレスが送信されるまでは完了です。

## パスワード登録画面（本登録画面）

メールに記載してある本登録画面が`http://localhost:3000/users/confirmation?confirmation_token=***`のようなURLになっています。`rails routes`で確認するとこれは`users/confirmations#show`に割り当ててありますのでそれを実装します。

コントローラーをカスタマイズする場合、対象のコントローラーのルートを指定する必要があります。

```rb:config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    :confirmations => 'users/confirmations',
  }
```

showアクションを実装します。

```rb:app/controllers/users/confirmations_controller.rb
class Users::ConfirmationsController < Devise::ConfirmationsController
  ...
  # GET /resource/confirmation?confirmation_token=abcdef
  def show
    self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token]) if params[:confirmation_token].present?
    super if resource.nil? or resource.confirmed?
  end
```

`confirmation_token`からuserのモデルを取得してメンバー変数にアサインしてます。

ビューはこんな感じ。

```erb:app/views/devise/confirmations/show.html.erb
<h2><%= "パスワードの登録" %></h2>

<%= form_for(resource, as: resource_name, url: users_confirm_path) do |f| %>
  <%= devise_error_messages! %>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "off" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "off" %>
  </div>

  <%= f.hidden_field :confirmation_token %>

  <div class="actions">
    <%= f.submit "登録" %>
  </div>
<% end %>
```

フォームのサブミット先の`users_confirm_path`はまだ存在しません。これから作ります。まずはルートから作ります。

```rb:config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    :confirmations => 'users/confirmations',
  }
  devise_scope :user do
    patch "users/confirm" => "users/confirmations#confirm"
  end
```

```rb:app/controllers/users/confirmations_controller.rb
class Users::ConfirmationsController < Devise::ConfirmationsController
  # GET /resource/confirmation?confirmation_token=abcdef
  def show
    self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token]) if params[:confirmation_token].present?
    super if resource.nil? or resource.confirmed?
  end

  def confirm
    confirmation_token = params[resource_name][:confirmation_token]
    self.resource = resource_class.find_by_confirmation_token!(confirmation_token)
    if resource.update_attributes(confirm_params)
      self.resource = resource_class.confirm_by_token(confirmation_token)
      set_flash_message :notice, :confirmed
      sign_in_and_redirect(resource_name, resource)
    else
      render :action => "show"
    end
  end

  private
    def confirm_params
      params.require(resource_name).permit(:password, :password_confirmation)
    end
end

```

送信されたパスワードと`confirmed_at`を現在時刻に更新しています。`confirmed_at`がnilのままだと`confirmed?`がfalseを返すので更新する必要があります。

以上です。最後にテストを書いたので載せておきます。

```rb:test/integration/sign_up_test.rb
require 'test_helper'

class SignUpTest < ActionDispatch::IntegrationTest
  test "入会時はメールアドレスだけで確認でき、有効化画面でパスワードで登録できます。" do
    get new_user_registration_path
    assert_template 'devise/registrations/new'

    assert_no_difference 'User.count' do
      post user_registration_path, params: { user: { email:  "" } }
    end
    assert_template 'devise/registrations/new'

    assert_difference 'User.count', 1 do
      post user_registration_path, params: { user: { email:  "sign_up_test@example.com" } }
    end
    follow_redirect!
    assert_template 'root_pages/index'

    user = User.find_by_email("sign_up_test@example.com")
    assert_not user.confirmed?
    assert_nil controller.current_user
    
    
    get user_confirmation_path(confirmation_token: user.confirmation_token)
    assert_template 'devise/confirmations/show'

    patch users_confirm_path, params: {user: {
      password: "",
      password_confirmation: "",
      confirmation_token: user.confirmation_token
    }}
    assert_not user.reload.confirmed?
    assert_template 'devise/confirmations/show'

    patch users_confirm_path, params: {user: {
      password: "password",
      password_confirmation: "password2",
      confirmation_token: user.confirmation_token
    }}
    assert_not user.reload.confirmed?
    assert_template 'devise/confirmations/show'
  
    patch users_confirm_path, params: {user: {
      password: "password",
      password_confirmation: "password",
      confirmation_token: user.confirmation_token
    }}
    follow_redirect!
    assert user.reload.confirmed?
    assert_template 'root_pages/index'
    assert_equal controller.current_user.email, "sign_up_test@example.com"
  end
end
```