Skip to content
7 changes: 4 additions & 3 deletions app/controllers/confirmations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ def create
protected

def after_confirmation_success(resource)
if resource.welcome_instructions_token.present?
if resource.previous_changes.key?("email")
redirect_to new_user_session_path, notice: "Your email has been confirmed. Please sign in."
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is now true to what we're actually trying to separate -- the email change flow vs the initial welcome flow

elsif resource.welcome_instructions_token.present?
redirect_to user_welcome_path(resource.welcome_instructions_token)
else
redirect_to new_user_session_path,
notice: "Your email has been confirmed. Please sign in."
redirect_to new_user_session_path, notice: "Your email has been confirmed. Please sign in."
end
end
end
4 changes: 2 additions & 2 deletions app/views/welcome/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
spellcheck: "false",
data: { password_toggle_target: "input" } %>
<button type="button" class="toggle-password" data-action="password-toggle#toggle" aria-label="Toggle password visibility">
<i class="fa fa-eye" data-password-toggle-target="eyeOpen"></i>
<i class="fa-solid fa-eye" data-password-toggle-target="eyeOpen"></i>
<svg data-password-toggle-target="eyeClosed" style="display:none" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 10c3 4 6.5 6 10 6s7-2 10-6"/><path d="M7.5 14l-1.5 3"/><path d="M12 16v3"/><path d="M16.5 14l1.5 3"/></svg>
</button>
</div>
Expand All @@ -56,7 +56,7 @@
spellcheck: "false",
data: { password_toggle_target: "input" } %>
<button type="button" class="toggle-password" data-action="password-toggle#toggle" aria-label="Toggle password visibility">
<i class="fa fa-eye" data-password-toggle-target="eyeOpen"></i>
<i class="fa-solid fa-eye" data-password-toggle-target="eyeOpen"></i>
<svg data-password-toggle-target="eyeClosed" style="display:none" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 10c3 4 6.5 6 10 6s7-2 10-6"/><path d="M7.5 14l-1.5 3"/><path d="M12 16v3"/><path d="M16.5 14l1.5 3"/></svg>
</button>
</div>
Expand Down
66 changes: 59 additions & 7 deletions spec/requests/user_invitation_flow_system_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,71 @@
expect(response).to redirect_to(user_welcome_path(user_with_token.welcome_instructions_token))
end

it "redirects to sign in if user has no welcome token" do
# User without welcome token
user_without_token = create(:user, confirmed_at: nil)
user_without_token.update_columns(welcome_instructions_token: nil)
user_without_token.send_confirmation_instructions
it "redirects to welcome page with existing token even if token is old" do
# User with old welcome token (tokens no longer expire)
user_with_old_token = create(:user, confirmed_at: nil)
user_with_old_token.set_welcome_instructions_token!
user_with_old_token.update_column(:welcome_instructions_created_at, 31.days.ago)
old_token = user_with_old_token.welcome_instructions_token
user_with_old_token.send_confirmation_instructions

# Visit confirmation link
get user_confirmation_path(confirmation_token: user_without_token.confirmation_token)
get user_confirmation_path(confirmation_token: user_with_old_token.confirmation_token)

# Should keep existing token and redirect to welcome page
user_with_old_token.reload
expect(user_with_old_token.welcome_instructions_token).to eq(old_token)
expect(response).to redirect_to(user_welcome_path(user_with_old_token.welcome_instructions_token))
end

it "redirects legacy ported users without welcome token to sign in" do
# Legacy user ported from old system — sign_in_count is 0, no welcome token
legacy_user = create(:user, confirmed_at: nil)
legacy_user.update_columns(welcome_instructions_token: nil, sign_in_count: 0)
legacy_user.send_confirmation_instructions

get user_confirmation_path(confirmation_token: legacy_user.confirmation_token)

# Should redirect to sign in page
expect(response).to redirect_to(new_user_session_path)
follow_redirect!
expect(response.body).to include("confirmed")
end

it "redirects to sign in on reconfirmation even with welcome token present" do
# Edge case: user has welcome token but is doing an email change
user = create(:user, confirmed_at: 1.year.ago)
user.set_welcome_instructions_token!
user.update_columns(
unconfirmed_email: "changed@example.com",
confirmation_token: Devise.friendly_token,
confirmation_sent_at: Time.current
)

get user_confirmation_path(confirmation_token: user.confirmation_token)

# Reconfirmation takes priority over welcome token
expect(response).to redirect_to(new_user_session_path)
end

it "redirects to sign in on email change reconfirmation" do
# Existing user changing their email — unconfirmed_email triggers reconfirmation detection
existing_user = create(:user, confirmed_at: 1.year.ago)
existing_user.update_columns(
unconfirmed_email: "newemail@example.com",
confirmation_token: Devise.friendly_token,
confirmation_sent_at: Time.current
)

get user_confirmation_path(confirmation_token: existing_user.confirmation_token)

expect(response).to redirect_to(new_user_session_path)
follow_redirect!
expect(response.body).to include("confirmed")

# Verify email was actually changed
existing_user.reload
expect(existing_user.email).to eq("newemail@example.com")
expect(existing_user.unconfirmed_email).to be_nil
end
end
end