[DON'T READ] How to customize Devise for Rails 7.0 and Turbo
Published: 2023-01-04 13:06:50

---
title: [DON'T READ] How to customize Devise for Rails 7.0 and Turbo
published: true
description: 
tags: 
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-01-04 12:33 +0000
---

🔔 NOTE: [Devise 4.9.0 has been released](https://github.com/heartcombo/devise/releases/tag/v4.9.0). Please consider upgrading Devise instead of reading this post.  

----

As of Devise 4.8.1, they say "[Turbo integration is not fully supported by Devise yet.](https://github.com/heartcombo/devise/blob/main/CHANGELOG.md#481---2021-12-16)" So you need to customize Devise settings for it. This post describes how to do it.

I tested against the following environments:

- Ruby on Rails 7.0.4
- Devise 4.8.1
- turbo-rails 1.3.2
- Ruby 3.2.0

And my post is a little modified from these articles (They helped me a lot!):

- https://gorails.com/episodes/devise-hotwire-turbo
- https://gorails.com/forum/how-to-use-devise-with-hotwire-turbo-js-discussion#forum_post_17983 

You can refer them to understand technical details.

## Preconditions

You already install Devise to your Rails app according to [Devise's README](https://github.com/heartcombo/devise/tree/v4.8.1).

## How to customize

Add the following code to `app/controllers/turbo_devise_controller.rb`:

```ruby 
class TurboDeviseController < ApplicationController
  class Responder < ActionController::Responder
    def to_turbo_stream
      if @default_response
        @default_response.call(options.merge(formats: :html))
      else
        controller.render(options.merge(formats: :html))
      end
    rescue ActionView::MissingTemplate => error
      if get?
        raise error
      elsif has_errors? && default_action
        if respond_to?(:error_rendering_options, true)
          # For responders 3.1.0 or higher
          render error_rendering_options.merge(formats: :html, status: :unprocessable_entity)
        else
          render rendering_options.merge(formats: :html, status: :unprocessable_entity)
        end
      else
        navigation_behavior error
      end
    end
  end

  self.responder = Responder
  respond_to :html, :turbo_stream
end
```

Then, open `config/initializers/devise.rb` and define TurboFailureApp class:

```ruby 
# frozen_string_literal: true

class TurboFailureApp < Devise::FailureApp
  def respond
    if request_format == :turbo_stream
      redirect
    else
      super
    end
  end

  alias skip_format? is_navigational_format?
end

# Assuming you have not yet modified this file, each configuration option below
# ...
```

Plus, apply the following change to `config/initializers/devise.rb`:


```diff
-  # config.parent_controller = 'DeviseController'
+  config.parent_controller = 'TurboDeviseController'
```

```diff
-  # config.navigational_formats = ['*/*', :html]
+  config.navigational_formats = ['*/*', :html, :turbo_stream]
```

```diff
-  # config.warden do |manager|
-  #   manager.intercept_401 = false
-  #   manager.default_strategies(scope: :user).unshift :some_external_strategy
-  # end
+  config.warden do |manager|
+    manager.failure_app = TurboFailureApp
+  end
```

Your sign-out link needs to replace `method: :delete` to `data: { turbo_method: :delete }`:

```diff  
-<%= link_to 'Sign out', destroy_user_session_path, method: :delete %>
+<%= link_to 'Sign out', destroy_user_session_path, data: { turbo_method: :delete } %>
```

If you have "Cancel my account" button in `app/views/devise/registrations/edit.html.erb`, you need to use `turbo_confirm` instead of `confirm` to pop-up confirmation dialog. Please run `rails generate devise:views` to generate customizable Devise views:

```diff
-<%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
+<%= button_to "Cancel my account", registration_path(resource_name), data: { turbo_confirm: "Are you sure?" }, method: :delete %>
```

That's all! Now you are ready to use Devise with Rails 7.0 and Turbo!

You can download and run my codes from here:
https://github.com/JunichiIto/devise-rails-7-sandbox

![Sign in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mm4jtd7ja964yg74nexg.png)
![Signed in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3l3zinn7rsxbi7p04wkv.png)
![Signed out](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ud4j0u518i3gchy730mr.png)

## Testing them
Here are my codes to test Devise's behavior.

You can see whole codes here:
https://github.com/JunichiIto/devise-rails-7-sandbox/tree/main/test/system

### Sign up

```ruby
# frozen_string_literal: true

require 'application_system_test_case'

class SignUpTest < ApplicationSystemTestCase
  test 'sign up' do
    visit root_path
    assert_css 'h2', text: 'Log in'
    click_link 'Sign up'

    # test validation errors
    click_button 'Sign up'
    assert_text '2 errors prohibited this user from being saved'

    fill_in 'Email', with: 'bob@example.com'
    fill_in 'Password', with: 'password'
    fill_in 'Password confirmation', with: 'password'
    click_button 'Sign up'
    assert_text 'Welcome! You have signed up successfully.'
    assert_text 'Account: bob@example.com'
    assert_css 'h1', text: 'Welcome!'
  end
end
```

### Sign in and sign out

```ruby
# frozen_string_literal: true

require 'application_system_test_case'

class SignInOutTest < ApplicationSystemTestCase
  test 'sign in and sign out' do
    visit root_path
    assert_css 'h2', text: 'Log in'

    # test validation errors
    fill_in 'Email', with: 'bob@example.com'
    fill_in 'Password', with: 'foobar'
    click_button 'Log in'
    assert_text 'Invalid Email or password.'

    fill_in 'Email', with: 'alice@example.com'
    fill_in 'Password', with: 'password'
    click_button 'Log in'
    assert_text 'Signed in successfully.'
    assert_css 'h1', text: 'Welcome!'

    click_link 'Sign out'
    assert_text 'Signed out successfully.'
    assert_css 'h2', text: 'Log in'
  end
end
```

### Edit account

```ruby
# frozen_string_literal: true

require 'application_system_test_case'

class EditAccountTest < ApplicationSystemTestCase
  setup do
    sign_in_as_alice
  end

  test 'edit account' do
    click_link 'Edit account'

    # test validation errors
    fill_in 'Email', with: ''
    click_button 'Update'
    assert_text '2 errors prohibited this user from being saved'

    fill_in 'Email', with: 'bob@example.com'
    fill_in 'Current password', with: 'password'
    click_button 'Update'
    assert_text 'Your account has been updated successfully.'
    assert_text 'Account: bob@example.com'
    assert_css 'h1', text: 'Welcome!'
  end
end
```

### Reset password

```ruby
# frozen_string_literal: true

require 'application_system_test_case'

class ResetPasswordTest < ApplicationSystemTestCase
  test 'reset password' do
    visit root_path
    assert_css 'h2', text: 'Log in'
    click_link 'Forgot your password?'

    # test validation errors
    click_button 'Send me reset password instructions'
    assert_text '1 error prohibited this user from being saved'

    fill_in 'Email', with: 'alice@example.com'
    click_button 'Send me reset password instructions'
    assert_text 'You will receive an email with instructions on how to reset your password in a few minutes.'
    assert_css 'h2', text: 'Log in'

    mail = ActionMailer::Base.deliveries.last
    m = mail.body.encoded.match(%r{http://example.com/(?<path>[-\w_?/=]+)})
    visit m[:path]
    assert_css 'h2', text: 'Change your password'

    # test validation errors
    click_button 'Change my password'
    assert_text '1 error prohibited this user from being saved'

    fill_in 'New password', with: 'pass1234'
    fill_in 'Confirm new password', with: 'pass1234'
    click_button 'Change my password'
    assert_text 'Your password has been changed successfully.'
    assert_css 'h1', text: 'Welcome!'
  end
end
```

### Cancel account

```ruby
# frozen_string_literal: true

require 'application_system_test_case'

class CancelAccountTest < ApplicationSystemTestCase
  setup do
    sign_in_as_alice
  end

  test 'cancel account' do
    click_link 'Edit account'
    accept_alert do
      click_button 'Cancel my account'
    end
    assert_text 'Bye! Your account has been successfully cancelled. We hope to see you again soon.'
    assert_css 'h2', text: 'Log in'
  end
end
```

## If you're using OmniAuth
If you're using OmniAuth to sign-in, you need to use `button_to` instead of `link_to`, and disable Turbo by `data: { turbo: false }`:

```diff
-<%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %><br />
+<%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %><br />
```

Please don't forget to use [omniauth-rails_csrf_protection](https://rubygems.org/gems/omniauth-rails_csrf_protection/) gem for OmniAuth 2.0 or higher. This requirement is not related to Turbo, though.