Merge pull request #1493 from basecamp/unify-signin-and-signup

Unify sign in and sign up
This commit is contained in:
Stanko Krtalić
2025-11-07 07:20:46 +01:00
committed by GitHub
37 changed files with 160 additions and 261 deletions
@@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base
include Authorization
include CurrentRequest, CurrentTimezone, SetPlatform
include TurboFlash, ViewTransitions
include Saas
stale_when_importmap_changes
allow_browser versions: :modern
+11
View File
@@ -0,0 +1,11 @@
module Saas
extend ActiveSupport::Concern
included do
helper_method :signups_allowed?
end
def signups_allowed?
defined?(Signup) && defined?(saas)
end
end
+2
View File
@@ -4,6 +4,8 @@ class JoinCodesController < ApplicationController
before_action :set_join_code
before_action :ensure_join_code_is_valid
layout "public"
def new
@account_name = ApplicationRecord.with_tenant(tenant) { Account.sole.name }
end
+14 -5
View File
@@ -1,4 +1,9 @@
class SessionsController < ApplicationController
# FIXME: Remove this before launch!
SIGNUP_USERNAME = Rails.env.test? ? "testname" : Rails.application.credentials.account_signup_http_basic_auth.name
SIGNUP_PASSWORD = Rails.env.test? ? "testpassword" : Rails.application.credentials.account_signup_http_basic_auth.password
http_basic_authenticate_with name: SIGNUP_USERNAME, password: SIGNUP_PASSWORD, realm: "Fizzy Signup", only: :create, unless: -> { Identity.exists?(email_address: email_address) }
require_untenanted_access
require_unauthenticated_access except: :destroy
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
@@ -9,11 +14,15 @@ class SessionsController < ApplicationController
end
def create
magic_link = Identity.find_by_email_address(email_address)&.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
redirect_to session_magic_link_path
if identity = Identity.find_by_email_address(email_address)
magic_link = identity.send_magic_link
flash[:magic_link_code] = magic_link&.code if Rails.env.development?
redirect_to session_magic_link_path
elsif signups_allowed?
Signup.new(email_address: email_address).create_identity
session[:return_to_after_authenticating] = saas.new_signup_membership_path
redirect_to session_magic_link_path
end
end
def destroy
@@ -3,6 +3,8 @@ class Users::JoinsController < ApplicationController
before_action :set_join_code, :ensure_join_code_is_valid
layout "public"
def new
end
+1 -1
View File
@@ -1,5 +1,5 @@
class ApplicationMailer < ActionMailer::Base
default from: "The Fizzy team <support@37signals.com>"
default from: "Fizzy <support@37signals.com>"
layout "mailer"
append_view_path Rails.root.join("app/views/mailers")
+1 -1
View File
@@ -3,7 +3,7 @@ class MagicLinkMailer < ApplicationMailer
@magic_link = magic_link
@identity = @magic_link.identity
mail to: @identity.email_address, subject: "Sign in to Fizzy"
mail to: @identity.email_address, subject: "Your Fizzy verification code"
end
private
-8
View File
@@ -1,8 +0,0 @@
class UserMailer < ApplicationMailer
def email_change_confirmation(user:, email_address:, token:)
@user = user
@token = token
mail to: email_address, subject: "Confirm your new email address"
end
end
+1 -1
View File
@@ -5,7 +5,7 @@ class Identity < UntenantedRecord
has_many :magic_links, dependent: :destroy
has_many :sessions, dependent: :destroy
normalizes :email_address, with: ->(value) { value.strip.downcase }
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
def send_magic_link
magic_links.create!.tap do |magic_link|
+3 -8
View File
@@ -1,12 +1,7 @@
<% @page_title = "Join #{@account_name}" %>
<% @page_title = "Join #{@account_name} in Fizzy" %>
<div class="panel panel--centered flex flex-column gap-half">
<header>
<h1 class="txt-x-large font-weight-black margin-none">
<%= @page_title %>
</h1>
<p class="margin-none-block-start">Enter your email address to continue</p>
</header>
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<%= form_with url: join_path(code: params[:code], tenant: params[:tenant]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
<div class="flex align-center gap">
@@ -16,7 +11,7 @@
</div>
<button type="submit" id="log_in" class="btn btn--link center" data-form-target="submit">
<span>Join</span>
<span>Continue</span>
<%= icon_tag "arrow-right" %>
</button>
<% end %>
+1 -1
View File
@@ -63,7 +63,7 @@
background: #2d71e5;
border-color: #2d71e5;
border-radius: 3rem;
color: #ffffff;
color: #ffffff !important;
font-weight: 500;
padding: 0.5em 1em;
text-decoration: none;
+3 -3
View File
@@ -6,9 +6,9 @@
<header class="header" id="header">
<a href="#main" class="header__skip-navigation btn" data-turbo="false">Skip to main content</a>
<nav>
<%= link_to "https://fizzy.do", class: "boxcar-header-logo center flex-inline align-center txt-normal" do %>
<%= image_tag "logo.png", alt: "Fizzy", role: "presentation" %>
<strong class="txt-medium overflow-ellipsis margin-none">Fizzy</strong>
<%= link_to "https://fizzy.do", class: "boxcar-header-logo center flex-inline align-center txt-large" do %>
<%= image_tag "logo.png" %>
<strong class="txt-large overflow-ellipsis margin-none">Fizzy</strong>
<% end %>
</nav>
@@ -1,5 +1,4 @@
<h1 class="title">Confirm your email address change</h1>
<p class="subtitle">Hit the button below to use this email address in Fizzy.</p>
<p class="subtitle">Confirm your email address change</p>
<%= link_to "Yes use use this email address", email_address_confirmation_url(membership_id: @membership.id, email_address_token: @token), class: "btn" %>
@@ -1,11 +1,13 @@
<h1 class="title">Sign in to Fizzy</h1>
<p class="subtitle">Hit the button below to sign in to Fizzy on this device</p>
<%= link_to "Sign in to Fizzy", session_magic_link_url(code: @magic_link.code), class: "btn" %>
<% if @identity.memberships.any? %>
<h1 class="title">Fizzy verification code</h1>
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-in page:</p>
<% else %>
<h1 class="title">Welcome to Fizzy!</h1>
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:</p>
<% end %>
<p class="margin-block-start-double">If youre signing in on another device, enter this special code:
<br><strong class="txt-large"><%= @magic_link.code %></strong>
</p>
<strong class="txt-large"><%= @magic_link.code %></strong>
<p class="footer">Need help? <%= mail_to "support@37signals.com", "Email us"%>.
</p>
@@ -1,8 +1,7 @@
Sign in to Fizzy
<%= "=" * 80 %>
<% if @identity.memberships.any? %>
Please enter this 6-character verification code on the Fizzy sign-in page:
<% else %>
Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:
<% end %>
Open this link in your browser to login on this device:
<%= session_magic_link_url(code: @magic_link.code) %>
If you're signing in on another device, enter this code:
<%= @magic_link.code %>
@@ -13,7 +13,7 @@
<h1 class="txt-x-large font-weight-black margin-none">
<%= @page_title %>
</h1>
<p class="margin-none">Enter your new email address. We'll send you a confirmation to verify it.</p>
<p class="margin-none">Enter your new email address, then hit the link to confirm it</p>
</header>
<%= form_with url: email_addresses_path(membership_id: @membership.id), method: :post, class: "flex flex-column gap", data: { controller: "form", turbo: false } do |form| %>
@@ -23,8 +23,8 @@
</label>
</div>
<button type="submit" class="btn btn--reversed center" data-form-target="submit">
<span>Next</span>
<button type="submit" class="btn btn--link center" data-form-target="submit">
<span>Continue</span>
<%= icon_tag "arrow-right" %>
</button>
<% end %>
+3 -5
View File
@@ -3,14 +3,12 @@
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
<header>
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<p class="margin-none-block-start txt-medium">We just emailed you a link to sign in.</p>
<p class="margin-none-block-start txt-medium">Then enter the verfifcation code included in the email below:</p>
</header>
<p class="margin-none-block-start margin-block-end-half txt-small txt-subtle">If your email is on a different device, enter the code included in the email below.</p>
<%= form_with url: session_magic_link_path, method: :post, html: { data: { controller: token_list("form", "auto-submit" => params[:code].present?)} } do |form| %>
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-normal",
autofocus: false, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
<%= form.text_field :code, required: true, class: "input center txt-align-enter txt-large",
autofocus: true, autocorrect: "off", autocapitalize: "off", spellcheck: "false", "data-1p-ignore": true,
autocomplete: "one-time-code", maxlength: "6", placeholder: "••••••", value: params[:code],
data: { form_target: "input", action: "paste->form#debouncedSubmit" } %>
<% end %>
+1 -1
View File
@@ -24,7 +24,7 @@
<p class="margin-none-block-start">You dont have any Fizzy accounts.</p>
<% end %>
<% if defined?(saas) %>
<% if signups_allowed? %>
<div class="margin-block-start">
<%= link_to saas.new_signup_membership_path, class: "btn btn--plain txt-link center txt-small", data: { turbo_prefetch: false } do %>
<span>Sign up for a new Fizzy account</span>
+8 -6
View File
@@ -1,17 +1,19 @@
<% @page_title = "Sign in" %>
<% @page_title = "Enter your email" %>
<div class="panel panel--centered flex flex-column gap-half">
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<div class="panel panel--centered flex flex-column gap-half" style="--panel-size: 65ch;">
<h1 class="txt-x-large font-weight-black margin-block-end">Lets get you in to Fizzy</h1>
<%= form_with url: session_path, class: "flex flex-column gap txt-medium" do |form| %>
<div class="flex align-center gap">
<label class="flex align-center gap input input--actor">
<%= form.email_field :email_address, required: true, class: "input full-width", autofocus: true, autocomplete: "username", placeholder: "Email address" %>
<%= form.email_field :email_address, required: true, class: "input txt-large full-width", autofocus: true, autocomplete: "username", placeholder: "Enter your email address" %>
</label>
</div>
<button type="submit" id="log_in" class="btn btn--link center">
<span>Continue</span>
<p>If youve already got an account well get you signed in. If youre new here, well set you up with your own Fizzy account.</p>
<button type="submit" id="log_in" class="btn btn--link center txt-medium">
<span>Lets go</span>
<%= icon_tag "arrow-right" %>
</button>
<% end %>
+4 -11
View File
@@ -1,24 +1,17 @@
<% @page_title = "Join #{Account.sole.name}" %>
<% @page_title = "Great! Now enter your name" %>
<div class="panel panel--centered flex flex-column gap-half">
<header>
<h1 class="txt-x-large font-weight-black margin-none">
<%= @page_title %>
</h1>
<p class="margin-none-block-start">Enter your name to continue</p>
</header>
<h1 class="txt-large font-weight-black margin-none"><%= @page_title %></h1>
<%= form_with scope: "user", url: users_joins_path(membership: params[:membership]), class: "flex flex-column gap txt-medium", data: { controller: "form" } do |form| %>
<div class="flex align-center gap">
<label class="flex align-center gap input input--actor">
<%= form.text_field :name, class: "input full-width", autocomplete: "name", placeholder: "Name", autofocus: true, required: true, data: { "1p-ignore": true } %>
<%= icon_tag "person", class: "txt-large" %>
<%= form.text_field :name, class: "input full-width", autocomplete: "name", placeholder: "Full name", autofocus: true, required: true, data: { "1p-ignore": true } %>
</label>
</div>
<button type="submit" id="log_in" class="btn btn--link center" data-form-target="submit">
<span>Join</span>
<span>Continue</span>
<%= icon_tag "arrow-right" %>
</button>
<% end %>
@@ -1,11 +0,0 @@
module Restricted
extend ActiveSupport::Concern
included do
unless Rails.env.development?
http_basic_authenticate_with \
name: Rails.env.test? ? "testname" : Rails.application.credentials.account_signup_http_basic_auth.name,
password: Rails.env.test? ? "testpassword" : Rails.application.credentials.account_signup_http_basic_auth.password
end
end
end
@@ -1,6 +1,4 @@
class Signups::CompletionsController < ApplicationController
include Restricted
class Signup::CompletionsController < ApplicationController
require_untenanted_access
layout "public"
@@ -1,6 +1,4 @@
class Signups::MembershipsController < ApplicationController
include Restricted
class Signup::MembershipsController < ApplicationController
require_untenanted_access
layout "public"
@@ -1,33 +0,0 @@
class SignupsController < ApplicationController
include Restricted
require_untenanted_access
require_unauthenticated_access
layout "public"
rate_limit only: :create, name: "short-term", to: 5, within: 3.minutes,
with: -> { redirect_to saas.new_signup_path, alert: "Try again later." }
rate_limit only: :create, name: "long-term", to: 10, within: 30.minutes,
with: -> { redirect_to saas.new_signup_path, alert: "Try again later." }
def new
@signup = Signup.new
end
def create
@signup = Signup.new(signup_params)
if @signup.create_identity
session[:return_to_after_authenticating] = saas.new_signup_membership_path
redirect_to session_magic_link_path
else
render :new, status: :unprocessable_entity
end
end
private
def signup_params
params.expect(signup: %i[ email_address ])
end
end
-6
View File
@@ -8,10 +8,6 @@ class Signup
attr_accessor :full_name, :email_address, :identity, :membership_id, :account_name
attr_reader :queenbee_account, :account, :user, :tenant, :membership
with_options on: :identity_creation do
validates_presence_of :email_address
end
with_options on: :membership_creation do
validates_presence_of :full_name, :identity
end
@@ -42,8 +38,6 @@ class Signup
end
def create_identity
return false unless valid?(:identity_creation)
@identity = Identity.find_or_create_by!(email_address: email_address)
@identity.send_magic_link
end
@@ -1,7 +1,7 @@
<% @page_title = "Creating your account" %>
<% @page_title = "Creating your account" %>
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<h1 class="txt-x-large font-weight-black margin-block-end"><%= @page_title %></h1>
<%= form_with model: @signup, url: saas.signup_completion_url(script_name: "/#{@signup.tenant}"), scope: "signup", class: "flex flex-column gap", data: { turbo: false, controller: @signup.errors.blank? ? "form auto-submit" : "form" } do |form| %>
<%= form.hidden_field :membership_id %>
@@ -19,8 +19,7 @@
<% end %>
<button type="submit" class="btn btn--link center" data-form-target="submit">
<span>Continue</span>
<%= icon_tag "arrow-right" %>
<span>Done</span>
</button>
<% end %>
</div>
@@ -1,12 +1,15 @@
<% @page_title = "Create your account" %>
<% @page_title = "Complete your sign-up" %>
<div class="panel panel--centered flex flex-column gap-half <%= "shake" if flash[:alert] %>">
<h1 class="txt-x-large font-weight-black margin-none"><%= @page_title %></h1>
<h1 class="txt-x-large font-weight-black margin-block-end"><%= @page_title %></h1>
<%= form_with model: @signup, url: saas.signup_membership_path, scope: "signup", class: "flex flex-column gap", data: { controller: "form" } do |form| %>
<%= form.hidden_field :new_user %>
<%= form.text_field :full_name, class: "input", autocomplete: "name", placeholder: "Your name", autofocus: true, required: true %>
<%= form.text_field :full_name, class: "input txt-large", autocomplete: "name", placeholder: "Enter your full name", autofocus: true, required: true %>
<p>Youre one step away. Just enter your name to get your own Fizzy account.</p>
<% if @signup.errors.any? %>
<div class="margin-block-half">
+5 -7
View File
@@ -1,11 +1,9 @@
Fizzy::Saas::Engine.routes.draw do
resource :signup, only: %i[ new create ] do
scope module: :signups, as: :signup do
collection do
resource :membership, only: %i[ new create ]
resource :completion, only: %i[ new create ]
end
end
get "/signup/new", to: redirect("/session/new")
namespace :signup do
resource :membership, only: %i[ new create ]
resource :completion, only: %i[ new create ]
end
Queenbee.routes(self)
@@ -1,6 +1,6 @@
require "test_helper"
class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
class Signup::CompletionsControllerTest < ActionDispatch::IntegrationTest
setup do
@signup = Signup.new(email_address: "newuser@example.com", full_name: "New User")
@@ -16,7 +16,7 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
get saas.new_signup_completion_path(signup: {
membership_id: @signup.membership_id,
full_name: @signup.full_name,
account_name: @signup.account_name }), headers: http_basic_auth_headers
account_name: @signup.account_name })
end
assert_response :success
@@ -30,7 +30,7 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
full_name: @signup.full_name,
account_name: @signup.account_name
}
}, headers: http_basic_auth_headers
}
end
assert_redirected_to landing_path(script_name: "/#{@signup.tenant}"), "Successful completion should redirect to root in new tenant"
@@ -42,14 +42,9 @@ class Signups::CompletionsControllerTest < ActionDispatch::IntegrationTest
full_name: "",
account_name: ""
}
}, headers: http_basic_auth_headers
}
end
assert_response :unprocessable_entity, "Invalid params should return unprocessable entity"
end
private
def http_basic_auth_headers
{ "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") }
end
end
@@ -1,6 +1,6 @@
require "test_helper"
class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
class Signup::MembershipsControllerTest < ActionDispatch::IntegrationTest
setup do
@identity = Identity.create!(email_address: "newuser@example.com")
magic_link = @identity.send_magic_link
@@ -16,7 +16,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
test "new" do
untenanted do
get saas.new_signup_membership_path, headers: http_basic_auth_headers
get saas.new_signup_membership_path
assert_response :success
end
@@ -24,7 +24,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
test "new with new_user param" do
untenanted do
get saas.new_signup_membership_path(signup: { new_user: true }), headers: http_basic_auth_headers
get saas.new_signup_membership_path(signup: { new_user: true })
assert_response :success
end
@@ -37,7 +37,7 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
signup: {
full_name: "New User"
}
}, headers: http_basic_auth_headers
}
end
membership = Membership.last
@@ -58,15 +58,10 @@ class Signups::MembershipsControllerTest < ActionDispatch::IntegrationTest
signup: {
full_name: ""
}
}, headers: http_basic_auth_headers
}
end
assert_response :unprocessable_entity, "Invalid params should return unprocessable entity"
end
end
private
def http_basic_auth_headers
{ "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword") }
end
end
@@ -1,54 +0,0 @@
require "test_helper"
class SignupsControllerTest < ActionDispatch::IntegrationTest
setup do
@signup_params = {
full_name: "Brian Wilson",
email_address: "brian@example.com",
company_name: "Beach Boys"
}
@starting_tenants = ApplicationRecord.tenants
# Clear script_name for untenanted signup tests
integration_session.default_url_options[:script_name] = nil
end
test "should require http basic authentication" do
get saas.new_signup_url
assert_response :unauthorized
end
test "should get new" do
get saas.new_signup_url, headers: http_basic_auth_headers
assert_response :success
assert_select "h1", "Sign up"
assert_select "input[name='signup[email_address]']"
end
test "should create signup and redirect to magic link page" do
assert_no_difference -> { ApplicationRecord.tenants.count } do
post saas.signup_url, params: { signup: { email_address: @signup_params[:email_address] } }, headers: http_basic_auth_headers
end
assert_redirected_to session_magic_link_path
end
test "should render new with errors when signup fails validation" do
invalid_params = { email_address: "" }
assert_no_difference -> { ApplicationRecord.tenants.count } do
post saas.signup_url, params: { signup: invalid_params }, headers: http_basic_auth_headers
end
assert_response :unprocessable_entity
assert_select ".txt-negative"
end
private
def http_basic_auth_headers
credentials = ActionController::HttpAuthentication::Basic.encode_credentials("testname", "testpassword")
{ "HTTP_AUTHORIZATION" => credentials }
end
end
+3 -3
View File
@@ -27,8 +27,9 @@ class SignupTest < ActiveSupport::TestCase
end
signup_invalid = Signup.new(email_address: "")
assert_not signup_invalid.create_identity, "Should fail with invalid email"
assert_not_empty signup_invalid.errors[:email_address], "Should have validation error for email_address"
assert_raises do
signup_invalid.create_identity
end
end
test "#create_membership" do
@@ -56,7 +57,6 @@ class SignupTest < ActiveSupport::TestCase
test "#complete" do
Account.any_instance.expects(:setup_customer_template).once
# First create the membership
signup_for_membership = Signup.new(
full_name: "Kevin",
identity: identities(:kevin)
+41 -28
View File
@@ -1,10 +1,46 @@
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
test "destroy" do
test "new" do
untenanted do
sign_in_as :kevin
get new_session_path
end
assert_response :success
end
test "create" do
identity = identities(:kevin)
untenanted do
assert_difference -> { MagicLink.count }, 1 do
post session_path, params: { email_address: identity.email_address }
end
assert_redirected_to session_magic_link_path
end
end
unless Bootstrap.oss_config?
test "create for a new user" do
untenanted do
assert_difference -> { Identity.count }, +1 do
assert_difference -> { MagicLink.count }, +1 do
post session_path,
params: { email_address: "nonexistent-#{SecureRandom.hex(6)}@example.com" },
headers: http_basic_auth_headers("testname", "testpassword")
end
end
assert_redirected_to session_magic_link_path
end
end
end
test "destroy" do
sign_in_as :kevin
untenanted do
delete session_path
assert_redirected_to new_session_path
@@ -12,31 +48,8 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
end
end
test "new" do
untenanted do
get new_session_path
assert_response :success
private
def http_basic_auth_headers(user, password)
{ "Authorization" => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) }
end
end
test "create" do
untenanted do
identity = identities(:kevin)
assert_difference -> { MagicLink.count }, 1 do
post session_path, params: { email_address: identity.email_address }
end
assert_redirected_to session_magic_link_path
end
untenanted do
assert_no_difference -> { MagicLink.count } do
post session_path, params: { email_address: "nonexistent@example.com" }
end
assert_redirected_to session_magic_link_path
end
end
end
+1 -1
View File
@@ -10,7 +10,7 @@ class MagicLinkMailerTest < ActionMailer::TestCase
end
assert_equal [ "kevin@37signals.com" ], email.to
assert_equal "Sign in to Fizzy", email.subject
assert_equal "Your Fizzy verification code", email.subject
assert_match magic_link.code, email.body.encoded
end
end
@@ -0,0 +1,17 @@
class IdentityMailerPreview < ActionMailer::Preview
def email_change_confirmation
ApplicationRecord.current_tenant = "897362094"
identity = Identity.find_by(email_address: "david@37signals.com")
membership = identity&.memberships&.find_by(tenant: ApplicationRecord.current_tenant)
new_email_address = "david.new@example.com"
token = membership.send(:generate_email_address_change_token, to: new_email_address)
IdentityMailer.email_change_confirmation(
email_address: new_email_address,
token: token,
membership: membership
)
end
end
@@ -1,18 +0,0 @@
class UserMailerPreview < ActionMailer::Preview
def email_change_confirmation
ApplicationRecord.current_tenant = "897362094"
user = User.find_by(email_address: "david@37signals.com") || User.new(
name: "David",
email_address: "david@37signals.com"
)
new_email_address = "david.new@example.com"
token = user.generate_email_address_change_token(to: new_email_address)
UserMailer.email_change_confirmation(
user: user,
email_address: new_email_address,
token: token
)
end
end