From 0ea3f38dbf1e9ce0c7d54b2dc96bd2a95b7a1511 Mon Sep 17 00:00:00 2001 From: Mac Siri Date: Mon, 9 Apr 2018 17:05:45 -0400 Subject: [PATCH] Implement one-click-unsubscribe (#192) * Implement one-click-unsubscribe feature WIP * Adjust email subscription page's css * Add mobile scss * Apply subscription logic to other email type * Add back default email footer * Create EmailSubscriptions request spec --- .../stylesheets/email_subscriptions.scss | 20 ++++++++++ app/assets/stylesheets/minimal.scss | 2 + .../email_subscriptions_controller.rb | 14 +++++++ app/labor/email_digest.rb | 8 ++-- app/mailers/application_mailer.rb | 10 ++++- app/mailers/digest_mailer.rb | 6 +-- app/mailers/notify_mailer.rb | 4 ++ .../email_subscriptions/unsubscribe.html.erb | 10 +++++ app/views/layouts/_mailer_content.html.erb | 1 - app/views/layouts/mailer.html.erb | 6 ++- config/routes.rb | 1 + spec/requests/email_subscriptions_spec.rb | 39 +++++++++++++++++++ 12 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 app/assets/stylesheets/email_subscriptions.scss create mode 100644 app/controllers/email_subscriptions_controller.rb create mode 100644 app/views/email_subscriptions/unsubscribe.html.erb create mode 100644 spec/requests/email_subscriptions_spec.rb diff --git a/app/assets/stylesheets/email_subscriptions.scss b/app/assets/stylesheets/email_subscriptions.scss new file mode 100644 index 000000000..731aa308f --- /dev/null +++ b/app/assets/stylesheets/email_subscriptions.scss @@ -0,0 +1,20 @@ +.unsubscribed { + background: #66e2d5; + color: white; + margin: 100px auto; + padding: 40px 10px; + + @media screen and (min-width: 550px) { + margin: 100px auto; + width: 450px; + padding: 40px; + } +} + +.unsubscribed__title { + font-weight: 600; +} + +.unsubscribed__message { + font-weight: 300; +} diff --git a/app/assets/stylesheets/minimal.scss b/app/assets/stylesheets/minimal.scss index 873897289..a26fd21ce 100644 --- a/app/assets/stylesheets/minimal.scss +++ b/app/assets/stylesheets/minimal.scss @@ -29,3 +29,5 @@ @import 'chat'; @import 'livechat/chat'; + +@import 'email_subscriptions'; diff --git a/app/controllers/email_subscriptions_controller.rb b/app/controllers/email_subscriptions_controller.rb new file mode 100644 index 000000000..01ab49ce5 --- /dev/null +++ b/app/controllers/email_subscriptions_controller.rb @@ -0,0 +1,14 @@ +class EmailSubscriptionsController < ApplicationController + def unsubscribe + verified_params = Rails.application.message_verifier(:unsubscribe).verify(params[:ut]) + + if verified_params[:expires_at] > Time.now + user = User.find(verified_params[:user_id]) + user.update(verified_params[:email_type] => false) + else + not_found + end + rescue ActiveSupport::MessageVerifier::InvalidSignature + not_found + end +end diff --git a/app/labor/email_digest.rb b/app/labor/email_digest.rb index f57611a4c..37b85a108 100644 --- a/app/labor/email_digest.rb +++ b/app/labor/email_digest.rb @@ -17,9 +17,11 @@ class EmailDigest user_email_heuristic = EmailLogic.new(user).analyze next unless user_email_heuristic.should_receive_email? articles = user_email_heuristic.articles_to_send - DigestMailer.digest_email(user, articles).deliver - rescue - puts "Email issue" + begin + DigestMailer.digest_email(user, articles).deliver + rescue StandardError + puts "Email issue" + end end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 598334661..05bd80276 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,12 @@ class ApplicationMailer < ActionMailer::Base default from: "The DEV Community " - layout 'mailer' + layout "mailer" + + def generate_unsubscribe_token(id, email_type) + Rails.application.message_verifier(:unsubscribe).generate( + user_id: id, + email_type: email_type.to_sym, + expires_at: Time.now + 2.days, + ) + end end diff --git a/app/mailers/digest_mailer.rb b/app/mailers/digest_mailer.rb index b8bb219fb..66452105a 100644 --- a/app/mailers/digest_mailer.rb +++ b/app/mailers/digest_mailer.rb @@ -6,6 +6,7 @@ class DigestMailer < ApplicationMailer user end @articles = articles.first(6) + @unsubscribe = generate_unsubscribe_token(@user.id, :email_digest_periodic) @digest_email = true mail(from: "DEV Digest ", to: @user.email, subject: "#{adjusted_title(@articles.first)} + #{@articles.size - 1} #{email_end_phrase} #{random_emoji}") do |format| format.html { render "layouts/mailer" } @@ -14,12 +15,11 @@ class DigestMailer < ApplicationMailer def adjusted_title(article) title = article.title.strip - - title = "\"#{title}\"" unless title.start_with? '"' + "\"#{title}\"" unless title.start_with? '"' end def random_emoji - ["🤓","🎉","🙈","🔥","💬","👋","👏","🐶","🦁","🐙","🦄","❤️","😇"].shuffle.take(3).join + ["🤓", "🎉", "🙈", "🔥", "💬", "👋", "👏", "🐶", "🦁", "🐙", "🦄", "❤️", "😇"].shuffle.take(3).join end def email_end_phrase diff --git a/app/mailers/notify_mailer.rb b/app/mailers/notify_mailer.rb index 6c8dd48c9..309e45827 100644 --- a/app/mailers/notify_mailer.rb +++ b/app/mailers/notify_mailer.rb @@ -7,6 +7,7 @@ class NotifyMailer < ApplicationMailer comment.parent_user end return if RateLimitChecker.new.limit_by_email_recipient_address(@user.email) + @unsubscribe = generate_unsubscribe_token(@user.id, :email_comment_notifications) @comment = comment mail(to: @user.email, subject: "#{@comment.user.name} replied to your #{@comment.parent_type}") do |format| format.html { render "layouts/mailer" } @@ -22,6 +23,7 @@ class NotifyMailer < ApplicationMailer end return if RateLimitChecker.new.limit_by_email_recipient_address(@user.email) @follower = follow.follower + @unsubscribe = generate_unsubscribe_token(@user.id, :email_follower_notifications) mail(to: @user.email, subject: "#{@follower.name} just followed you on dev.to") do |format| format.html { render 'layouts/mailer' } @@ -35,6 +37,7 @@ class NotifyMailer < ApplicationMailer @mentioner = User.find(mention.mentionable.user_id) @mentionable = mention.mentionable @mention = mention + @unsubscribe = generate_unsubscribe_token(@user.id, :email_mention_notifications) mail(to: @user.email, subject: "#{@mentioner.name} just mentioned you!") do |format| format.html { render 'layouts/mailer' } @@ -50,6 +53,7 @@ class NotifyMailer < ApplicationMailer end return if RateLimitChecker.new.limit_by_email_recipient_address(@user.email) @unread_notifications_count = NotificationCounter.new(@user).unread_notification_count + @unsubscribe = generate_unsubscribe_token(@user.id, :email_unread_notifications) mail(to: @user.email, subject: "🔥 You have #{@unread_notifications_count} unread notifications on dev.to") do |format| format.html { render 'layouts/mailer' } format.text { render plain: "Visit https://dev.to/notifications to read all of your notifications" } diff --git a/app/views/email_subscriptions/unsubscribe.html.erb b/app/views/email_subscriptions/unsubscribe.html.erb new file mode 100644 index 000000000..f7cee2041 --- /dev/null +++ b/app/views/email_subscriptions/unsubscribe.html.erb @@ -0,0 +1,10 @@ +
+
+ You have been successfully unsubscribed. 😔 +
+
+
+ If that was a mistake or you would like to update other email preferences, + click here. +
+
diff --git a/app/views/layouts/_mailer_content.html.erb b/app/views/layouts/_mailer_content.html.erb index 504982684..0e62c7631 100644 --- a/app/views/layouts/_mailer_content.html.erb +++ b/app/views/layouts/_mailer_content.html.erb @@ -219,5 +219,4 @@ <% end %> - <% end %> diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index ecf4fd410..81ededaef 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -8,7 +8,11 @@ - Don't want to get emails like this? Adjust your email settings at dev.to/settings/notifications + <%if @unsubscribe %> + <%= link_to "Unsubscribe", email_subscriptions_unsubscribe_url(ut: @unsubscribe) %> | Adjust your email settings + <% else %> + Don't want to get emails like this? Adjust your email settings at dev.to/settings/notifications + <% end %>

<% if @user.twitter_username.blank? %> Reminder: You used GitHub to authenticate your account, so use that to sign in if prompted. diff --git a/config/routes.rb b/config/routes.rb index 96cc50888..a0180f661 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,6 +91,7 @@ Rails.application.routes.draw do resources :additional_content_boxes, only: [:index] get "/notifications/:username" => "notifications#index" patch "/onboarding_update" => "users#onboarding_update" + get "email_subscriptions/unsubscribe" # resources :users ### Subscription vanity url diff --git a/spec/requests/email_subscriptions_spec.rb b/spec/requests/email_subscriptions_spec.rb new file mode 100644 index 000000000..de68b6553 --- /dev/null +++ b/spec/requests/email_subscriptions_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" + +RSpec.describe "EmailSubscriptions", type: :request do + let(:user) { create(:user) } + + def generate_token(user_id) + Rails.application.message_verifier(:unsubscribe).generate( + user_id: user_id, + email_type: :email_mention_notifications, + expires_at: Time.now + 2.days, + ) + end + + describe "GET /email_subscriptions/unsubscribe" do + it "returns 200 if valid" do + get email_subscriptions_unsubscribe_url(ut: generate_token(user.id)) + expect(response.status).to be(200) + end + + it "does unsubscribe the user" do + get email_subscriptions_unsubscribe_url(ut: generate_token(user.id)) + user.reload + expect(user.email_mention_notifications).to be(false) + end + + it "handles error properly" do + expect { get email_subscriptions_unsubscribe_url }. + to raise_error(ActionController::RoutingError) + end + + it "won't work if it's past expireation date" do + token = generate_token(user.id) + Timecop.freeze(Date.today + 3) do + expect { get email_subscriptions_unsubscribe_url(ut: token) }. + to raise_error(ActionController::RoutingError) + end + end + end +end