Make sign up Magic Links work across devices
Previously if someone started signing in on their phone, but finished it on their laptop, they'd end up on the menu screen with no account. Bu tracking the purpose of a Magic Link we can always direct the user to sign up if they requested a magic link through sign up. This also makes the logic for changing the copy in the email more robust.
This commit is contained in:
@@ -9,9 +9,9 @@ class Sessions::MagicLinksController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if identity = MagicLink.consume(code)
|
if magic_link = MagicLink.consume(code)
|
||||||
start_new_session_for identity
|
start_new_session_for magic_link.identity
|
||||||
redirect_to after_authentication_url
|
redirect_to after_sign_in_url(magic_link)
|
||||||
else
|
else
|
||||||
redirect_to session_magic_link_path, alert: "Try another code."
|
redirect_to session_magic_link_path, alert: "Try another code."
|
||||||
end
|
end
|
||||||
@@ -21,4 +21,12 @@ class Sessions::MagicLinksController < ApplicationController
|
|||||||
def code
|
def code
|
||||||
params.expect(:code)
|
params.expect(:code)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def after_sign_in_url(magic_link)
|
||||||
|
if magic_link.for_sign_up?
|
||||||
|
new_signup_completion_path
|
||||||
|
else
|
||||||
|
after_authentication_url
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ class Identity < ApplicationRecord
|
|||||||
|
|
||||||
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
|
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
|
||||||
|
|
||||||
def send_magic_link
|
def send_magic_link(**attributes)
|
||||||
magic_links.create!.tap do |magic_link|
|
attributes[:purpose] = attributes.delete(:for) if attributes.key?(:for)
|
||||||
|
|
||||||
|
magic_links.create!(attributes).tap do |magic_link|
|
||||||
MagicLinkMailer.sign_in_instructions(magic_link).deliver_later
|
MagicLinkMailer.sign_in_instructions(magic_link).deliver_later
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ class MagicLink < ApplicationRecord
|
|||||||
|
|
||||||
belongs_to :identity
|
belongs_to :identity
|
||||||
|
|
||||||
|
enum :purpose, %w[ sign_in sign_up ], prefix: :for, default: :sign_in
|
||||||
|
|
||||||
scope :active, -> { where(expires_at: Time.current...) }
|
scope :active, -> { where(expires_at: Time.current...) }
|
||||||
scope :stale, -> { where(expires_at: ..Time.current) }
|
scope :stale, -> { where(expires_at: ..Time.current) }
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ class MagicLink < ApplicationRecord
|
|||||||
|
|
||||||
def consume
|
def consume
|
||||||
destroy
|
destroy
|
||||||
identity
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Signup
|
|||||||
|
|
||||||
def create_identity
|
def create_identity
|
||||||
@identity = Identity.find_or_create_by!(email_address: email_address)
|
@identity = Identity.find_or_create_by!(email_address: email_address)
|
||||||
@identity.send_magic_link
|
@identity.send_magic_link for: :sign_up
|
||||||
end
|
end
|
||||||
|
|
||||||
def complete
|
def complete
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
<% if @identity.users.any? %>
|
<% if @magic_link.for_sign_in? %>
|
||||||
<h1 class="title">Fizzy verification code</h1>
|
<h1 class="title">Fizzy verification code</h1>
|
||||||
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-in page:</p>
|
<p class="subtitle">Please enter this 6-character verification code on the Fizzy sign-in page:</p>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% if @identity.users.any? %>
|
<% if @magic_link.for_sign_in? %>
|
||||||
Please enter this 6-character verification code on the Fizzy sign-in page:
|
Please enter this 6-character verification code on the Fizzy sign-in page:
|
||||||
<% else %>
|
<% else %>
|
||||||
Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:
|
Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class AddPurposeToMagicLinks < ActiveRecord::Migration[8.2]
|
||||||
|
def change
|
||||||
|
add_column :magic_links, :purpose, :integer, null: true
|
||||||
|
|
||||||
|
execute <<-SQL
|
||||||
|
UPDATE magic_links SET purpose = 0
|
||||||
|
SQL
|
||||||
|
|
||||||
|
change_column_null :magic_links, :purpose, false
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+2
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.2].define(version: 2025_11_27_000001) do
|
ActiveRecord::Schema[8.2].define(version: 2025_11_29_110120) do
|
||||||
create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "accesses", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.datetime "accessed_at"
|
t.datetime "accessed_at"
|
||||||
t.uuid "account_id", null: false
|
t.uuid "account_id", null: false
|
||||||
@@ -315,6 +315,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_27_000001) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "expires_at", null: false
|
t.datetime "expires_at", null: false
|
||||||
t.uuid "identity_id"
|
t.uuid "identity_id"
|
||||||
|
t.integer "purpose", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["code"], name: "index_magic_links_on_code", unique: true
|
t.index ["code"], name: "index_magic_links_on_code", unique: true
|
||||||
t.index ["expires_at"], name: "index_magic_links_on_expires_at"
|
t.index ["expires_at"], name: "index_magic_links_on_expires_at"
|
||||||
|
|||||||
@@ -9,17 +9,32 @@ class Sessions::MagicLinksControllerTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create" do
|
test "create with sign in code" do
|
||||||
identity = identities(:kevin)
|
identity = identities(:kevin)
|
||||||
magic_link = MagicLink.create!(identity: identity)
|
magic_link = MagicLink.create!(identity: identity)
|
||||||
|
|
||||||
untenanted do
|
untenanted do
|
||||||
post session_magic_link_url, params: { code: magic_link.code }
|
post session_magic_link_url, params: { code: magic_link.code }
|
||||||
end
|
|
||||||
|
|
||||||
assert_response :redirect
|
assert_response :redirect
|
||||||
assert cookies[:session_token].present?
|
assert cookies[:session_token].present?
|
||||||
assert_not MagicLink.exists?(magic_link.id), "The magic link should be consumed"
|
assert_redirected_to landing_path, "Should redirect to after authentication path"
|
||||||
|
assert_not MagicLink.exists?(magic_link.id), "The magic link should be consumed"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create with sign up code" do
|
||||||
|
identity = identities(:kevin)
|
||||||
|
magic_link = MagicLink.create!(identity: identity, purpose: :sign_up)
|
||||||
|
|
||||||
|
untenanted do
|
||||||
|
post session_magic_link_url, params: { code: magic_link.code }
|
||||||
|
|
||||||
|
assert_response :redirect
|
||||||
|
assert cookies[:session_token].present?
|
||||||
|
assert_redirected_to new_signup_completion_path, "Should redirect to signup completion"
|
||||||
|
assert_not MagicLink.exists?(magic_link.id), "The magic link should be consumed"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create with invalid code" do
|
test "create with invalid code" do
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ class MagicLinkTest < ActiveSupport::TestCase
|
|||||||
magic_link = MagicLink.create!(identity: identities(:kevin))
|
magic_link = MagicLink.create!(identity: identities(:kevin))
|
||||||
code_with_spaces = magic_link.code.downcase.chars.join(" ")
|
code_with_spaces = magic_link.code.downcase.chars.join(" ")
|
||||||
|
|
||||||
identity = MagicLink.consume(code_with_spaces)
|
consumed_magic_link = MagicLink.consume(code_with_spaces)
|
||||||
assert_equal identities(:kevin), identity
|
assert_equal magic_link, consumed_magic_link
|
||||||
assert_not MagicLink.exists?(magic_link.id)
|
assert_not MagicLink.exists?(magic_link.id)
|
||||||
|
|
||||||
expired_link = MagicLink.create!(identity: identities(:kevin))
|
expired_link = MagicLink.create!(identity: identities(:kevin))
|
||||||
|
|||||||
Reference in New Issue
Block a user