Skip to content

Commit 126d925

Browse files
committed
Implement authenticated subscribe/unsubscribe with hotwire
Add subscribe and unsubscribe buttons for logged in users since we know their email and subscription, if it exists. The UX is implemented takes advantage of hotwire. We don’t have flash messages styled nicely yet or working with Hotwire—future improvement needed.
1 parent b63bb5e commit 126d925

File tree

11 files changed

+193
-27
lines changed

11 files changed

+193
-27
lines changed

app/controllers/users/newsletter_subscriptions_controller.rb

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
class Users::NewsletterSubscriptionsController < ApplicationController
22
invisible_captcha only: [:create]
33

4-
before_action :authenticate_user_or_not_found!, only: [:index]
4+
before_action :authenticate_user_or_not_found!, only: [:index, :subscribe]
5+
before_action :authenticate_user!, only: [:subscribe]
6+
before_action :redirect_if_authenticated, only: [:create]
57

68
def index
79
@newsletter_subscription = current_user.newsletter_subscription || current_user.build_newsletter_subscription
810

9-
render Users::NewsletterSubscriptions::ShowView.new(newsletter_subscription: @newsletter_subscription)
11+
render Users::NewsletterSubscriptions::ShowView.new(newsletter_subscription: @newsletter_subscription, show_unsubscribe: true)
1012
end
1113

1214
def show
1315
@newsletter_subscription = NewsletterSubscription.find(params[:id]) or raise ActiveRecord::RecordNotFound
1416

15-
render Users::NewsletterSubscriptions::ShowView.new(newsletter_subscription: @newsletter_subscription)
17+
render Users::NewsletterSubscriptions::ShowView.new(newsletter_subscription: @newsletter_subscription, show_unsubscribe: false)
1618
end
1719

1820
def new
19-
@newsletter_subscription = current_user&.newsletter_subscription || NewsletterSubscription.new do |ns|
20-
ns.subscriber = current_user || User.new
21-
end
21+
user = current_user || User.new
22+
@newsletter_subscription = user.newsletter_subscription || user.build_newsletter_subscription
2223

2324
render Users::NewsletterSubscriptions::NewView.new(newsletter_subscription: @newsletter_subscription)
2425
end
@@ -29,12 +30,9 @@ def create
2930
u.subscribing = true
3031
end
3132

32-
if !@user.subscribed_to_newsletter?
33-
@user.build_newsletter_subscription
34-
@user.save
35-
end
33+
@newsletter_subscription = @user.newsletter_subscription || @user.build_newsletter_subscription
3634

37-
@newsletter_subscription = @user.newsletter_subscription
35+
@user.save
3836

3937
if @user.errors.any?
4038
return render Users::NewsletterSubscriptions::NewView.new(newsletter_subscription: @newsletter_subscription), status: :unprocessable_entity
@@ -50,6 +48,23 @@ def create
5048
notice: "Welcome to Joy of Rails! Please check your email for confirmation instructions"
5149
end
5250

51+
def subscribe
52+
if !current_user.subscribed_to_newsletter?
53+
current_user.create_newsletter_subscription
54+
end
55+
56+
@newsletter_subscription = current_user.newsletter_subscription
57+
58+
if current_user.needs_confirmation?
59+
EmailConfirmationNotifier.deliver_to(current_user)
60+
end
61+
62+
respond_to do |format|
63+
format.html { redirect_to root_path, notice: "Success!" }
64+
format.turbo_stream { redirect_to users_newsletter_subscription_path(@newsletter_subscription) }
65+
end
66+
end
67+
5368
def unsubscribe
5469
if params[:token]
5570
subscription = NewsletterSubscription.find_by_token_for(:unsubscribe, params[:token]) or raise ActiveRecord::RecordNotFound
@@ -67,7 +82,13 @@ def unsubscribe
6782
# could render show action instead
6883
render plain: "You have been unsubscribed", status: :ok
6984
else
70-
redirect_to root_path, notice: "You have been unsubscribed"
85+
respond_to do |format|
86+
format.html { redirect_to root_path, notice: "You have been unsubscribed" }
87+
format.turbo_stream {
88+
redirect_path = current_user ? users_newsletter_subscriptions_path : new_users_newsletter_subscription_path
89+
redirect_to redirect_path
90+
}
91+
end
7192
end
7293
end
7394
end

app/javascript/css/components/button.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
color: rgb(255 255 255 / var(--tw-text-opacity));
1212
line-height: inherit;
1313

14+
&:is(a) {
15+
text-decoration: none;
16+
}
17+
1418
&:hover {
1519
cursor: pointer;
1620
--tw-text-opacity: 1;
@@ -30,6 +34,9 @@
3034
&.warn {
3135
background-color: var(--joy-button-warn);
3236
}
37+
&.danger {
38+
background-color: var(--joy-button-warn);
39+
}
3340

3441
fieldset[disabled] &,
3542
&[disabled] {

app/models/user.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def confirmed?
5959

6060
def unconfirmed? = !confirmed?
6161

62-
def subscribed_to_newsletter? = newsletter_subscription.present?
62+
def subscribed_to_newsletter? = !!newsletter_subscription&.persisted?
6363

6464
# We want built-in validations provided by has_secure_password but to make
6565
# room for users who are only subscribing for newsletter content, we need to

app/views/users/newsletter_subscriptions/form.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def initialize(newsletter_subscription:)
1111
end
1212

1313
def view_template
14-
form_with model: @newsletter_subscription.subscriber, url: form_url, class: "lg:w-1/2" do |f|
14+
form_with model: @newsletter_subscription.subscriber, url: form_url, method: :post, class: "lg:w-1/2" do |f|
1515
invisible_captcha
1616
div(class: "flex flex-row items-center mt-2") do
1717
div(class: "flex-grow mr-2") do

app/views/users/newsletter_subscriptions/new_view.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ def initialize(newsletter_subscription:)
1010
def view_template
1111
render Users::NewsletterSubscriptions::Banner.new do
1212
turbo_frame_tag :newsletter_subscription do
13-
render Users::NewsletterSubscriptions::Form.new(newsletter_subscription: @newsletter_subscription)
13+
if helpers.user_signed_in?
14+
div do
15+
render Users::NewsletterSubscriptions::SubscribeButton.new
16+
end
17+
else
18+
render Users::NewsletterSubscriptions::Form.new(newsletter_subscription: @newsletter_subscription)
19+
end
1420
end
1521
end
1622
end

app/views/users/newsletter_subscriptions/show_view.rb

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,41 @@
33
class Users::NewsletterSubscriptions::ShowView < ApplicationView
44
include Phlex::Rails::Helpers::TurboFrameTag
55

6-
def initialize(newsletter_subscription:)
6+
def initialize(newsletter_subscription:, show_unsubscribe: false)
77
@newsletter_subscription = newsletter_subscription
8+
@show_unsubscribe = show_unsubscribe
89
end
910

1011
def view_template
1112
render Users::NewsletterSubscriptions::Banner.new do
1213
turbo_frame_tag :newsletter_subscription do
13-
div(class: "bg-success callout flex flex-row items-center mt-2") do
14-
div(class: "flex-grow mr-2") do
15-
plain "You are subscribed to the Joy of Rails newsletter!"
16-
if @newsletter_subscription.subscriber.needs_confirmation?
17-
whitespace
18-
plain "Please check your email for a confirmation link."
19-
# TODO:
20-
# Didn't receive it?
21-
# = button_to "Resend confirmation email", users_confirmations_path, params: { user: { email: newsletter_subscription.subscriber.email }}, class: "button primary"
14+
if @newsletter_subscription.persisted?
15+
div(class: "bg-success callout flex flex-row items-center mt-2") do
16+
div(class: "flex-grow mr-2") do
17+
subscribed_message
2218
end
19+
20+
if @show_unsubscribe && helpers.user_signed_in?
21+
render Users::NewsletterSubscriptions::UnsubscribeButton.new
22+
end
23+
end
24+
elsif helpers.user_signed_in?
25+
div do
26+
render Users::NewsletterSubscriptions::SubscribeButton.new
2327
end
2428
end
2529
end
2630
end
2731
end
32+
33+
def subscribed_message
34+
plain "You are subscribed to the Joy of Rails newsletter!"
35+
if @newsletter_subscription.subscriber.needs_confirmation?
36+
whitespace
37+
plain "Please check your email for a confirmation link."
38+
# TODO:
39+
# Didn't receive it?
40+
# = button_to "Resend confirmation email", users_confirmations_path, params: { user: { email: newsletter_subscription.subscriber.email }}, class: "button primary"
41+
end
42+
end
2843
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class Users::NewsletterSubscriptions::SubscribeButton < ApplicationComponent
2+
include Phlex::Rails::Helpers::LinkTo
3+
include Phlex::Rails::Helpers::ButtonTo
4+
5+
def view_template
6+
return plain "" unless helpers.user_signed_in?
7+
8+
if helpers.current_user.subscribed_to_newsletter?
9+
link_to "Manage subscription", users_newsletter_subscriptions_path, class: "button secondary"
10+
else
11+
button_to "Subscribe", subscribe_users_newsletter_subscriptions_path, class: "button primary"
12+
end
13+
end
14+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class Users::NewsletterSubscriptions::UnsubscribeButton < ApplicationComponent
2+
include Phlex::Rails::Helpers::ButtonTo
3+
4+
# TODO: Make this button work for users who are not signed in—first we need to figure out when to show it for them
5+
def view_template
6+
button_to "Unsubscribe", unsubscribe_users_newsletter_subscriptions_path, class: "button secondary",
7+
form: {data: {turbo_confirm: "Just confirming: Are you sure you want to unsubscribe?"}}
8+
end
9+
end

config/routes.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@
3737
resources :confirmations, only: [:new, :create, :edit, :update], param: :token
3838
resources :passwords, only: [:new, :create, :edit, :update], param: :token
3939

40-
resources :newsletter_subscriptions, only: [:new, :create, :index, :show]
40+
resources :newsletter_subscriptions, only: [:new, :create, :index, :show] do
41+
collection do
42+
post :subscribe
43+
end
44+
end
45+
4146
resources :newsletter_subscriptions, only: [], param: :token do
4247
match :unsubscribe, on: :collection, via: [:get, :post, :delete]
4348
match :unsubscribe, on: :member, via: [:get, :post, :delete]

spec/requests/users/newsletter_subscriptions_spec.rb

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
expect(mail.subject).to eq "Confirm your email address"
8282
end
8383

84-
it "quietly succeeds for a user subscribing with an already subscribed email" do
84+
it "disallows for a subscribing with an already subscribed email" do
8585
user = FactoryBot.create(:user, :subscribed)
8686

8787
expect {
@@ -131,6 +131,63 @@
131131
end
132132
end
133133

134+
describe "POST subscribe" do
135+
it "subscribes unsubscribed, logged-in user" do
136+
user = FactoryBot.create(:user, :unsubscribed)
137+
login_user user
138+
139+
expect(user.newsletter_subscription).to be_nil
140+
141+
expect {
142+
post subscribe_users_newsletter_subscriptions_path
143+
}.to change(NewsletterSubscription, :count).by(1)
144+
145+
expect(response).to redirect_to(root_path)
146+
expect(flash[:notice]).to eq("Success!")
147+
148+
expect(user.reload.newsletter_subscription).to be_present
149+
end
150+
151+
it "sends confirmation email" do
152+
user = FactoryBot.create(:user, :unconfirmed, :unsubscribed)
153+
login_user user
154+
155+
post subscribe_users_newsletter_subscriptions_path
156+
157+
expect(user.reload.newsletter_subscription).to be_present
158+
159+
perform_enqueued_jobs_and_subsequently_enqueued_jobs
160+
161+
mail = find_mail_to(user.email)
162+
163+
expect(mail.subject).to eq "Confirm your email address"
164+
end
165+
166+
it "quietly succeeds for already subscribed user" do
167+
user = FactoryBot.create(:user, :subscribed)
168+
login_user user
169+
170+
expect(user.newsletter_subscription).to be_present
171+
172+
expect {
173+
post subscribe_users_newsletter_subscriptions_path
174+
}.not_to change(NewsletterSubscription, :count)
175+
176+
expect(response).to redirect_to(root_path)
177+
expect(flash[:notice]).to eq("Success!")
178+
179+
expect(user.reload.newsletter_subscription).to be_present
180+
end
181+
182+
it "disallows for unauthenticated user" do
183+
expect {
184+
post subscribe_users_newsletter_subscriptions_path
185+
}.not_to change(NewsletterSubscription, :count)
186+
187+
expect(response).to have_http_status(:not_found)
188+
end
189+
end
190+
134191
describe "POST unsubscribe" do
135192
it "unsubscribes a user with an existing subscription and unsubscribe token via GET" do
136193
user = FactoryBot.create(:user, :subscribed)

0 commit comments

Comments
 (0)