From a49f71f2ba18563b1994849d497f9fab447d2283 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 14 Oct 2019 14:00:19 +0200 Subject: [PATCH 01/11] chore: disable forgery protection on api calls --- app/controllers/application_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 04d1c9ac..114ae9ef 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ApplicationController < ActionController::Base + protect_from_forgery unless: -> { request.format.json? } + helper_method :current_user, :logged_in? def current_user From 0f077efd25684d162c985bd3660a8f5934327d9b Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 14 Oct 2019 14:00:43 +0200 Subject: [PATCH 02/11] feat: add subscription cancel endpoint --- app/controllers/subscriptions_controller.rb | 26 +++++++++++++++++++++ config/routes.rb | 1 + 2 files changed, 27 insertions(+) create mode 100644 app/controllers/subscriptions_controller.rb diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb new file mode 100644 index 00000000..40cf4671 --- /dev/null +++ b/app/controllers/subscriptions_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class SubscriptionsController < ApplicationController + before_action :set_user, only: %i[destroy] + + # DELETE /user/:user_id/subscription + # DELETE /user/:user_id/subscription.json + def destroy + head :not_authorized unless current_user == @user + + subscription = @user.subscription + + subscription.active = false + if subscription.save + head :ok + else + head :bad_request + end + end + + private + + def set_user + @user = User.find(params[:user_id]) + end +end diff --git a/config/routes.rb b/config/routes.rb index 6a019e3e..2a4edfbc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,6 +21,7 @@ resources :users, only: %i[show new edit update create destroy] do resource :streak, only: %i[show] resources :cards, only: %i[index destroy] + resource :subscription, only: %i[destroy] end get '/login' => 'sessions#login' From 28f7ff73634e58491a4d754da7f50e004210d13b Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 14 Oct 2019 14:01:01 +0200 Subject: [PATCH 03/11] test: add subscription cancel from user profile --- spec/features/user/users_profile_spec.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/spec/features/user/users_profile_spec.rb b/spec/features/user/users_profile_spec.rb index 13c9b03a..bcaaf359 100644 --- a/spec/features/user/users_profile_spec.rb +++ b/spec/features/user/users_profile_spec.rb @@ -5,7 +5,7 @@ describe 'User - manages their profile', type: :feature, js: true do describe 'home' do let!(:user) { FactoryBot.create(:user) } - let!(:subscription) { FactoryBot.create(:subscription, user_id: user.id) } + let!(:subscription) { FactoryBot.create(:subscription, user_id: user.id, active: true) } let!(:one_time_donations) { FactoryBot.create_list(:donation, 5, user_id: user.id, donation_type: Donation::DONATION_TYPES[:one_off]) } before(:each) do @@ -27,5 +27,25 @@ expect(page).to have_content(donation.amount) end end + + it 'can cancel an active subscription' do + expect(subscription.active).to eq(true) + visit user_path(user) + expect(page).to have_content(user.name) + expect(page).to have_content(user.email) + + expect(page).to have_content(subscription.plan.name) + + click_button 'Cancel Subscription' + + expect(page).to have_content('Do you want to terminate your current subscription?') + within '#cancel-subscription-dialog' do + click_button 'Cancel Subscription' + end + + sleep 2 + subscription.reload + expect(subscription.active).to eq(false) + end end end From 2cd19bf39c0903bbaa33558c281f834a33649b1c Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 14 Oct 2019 14:01:35 +0200 Subject: [PATCH 04/11] feat: add subscription cancel dialog --- .../User/components/SubscriptionCancel.jsx | 72 +++++++++++++++++++ .../bundles/User/components/UserShow.jsx | 29 ++++---- app/javascript/packs/user-bundle.js | 4 +- app/views/users/show.html.erb | 1 + 4 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 app/javascript/bundles/User/components/SubscriptionCancel.jsx diff --git a/app/javascript/bundles/User/components/SubscriptionCancel.jsx b/app/javascript/bundles/User/components/SubscriptionCancel.jsx new file mode 100644 index 00000000..c326e486 --- /dev/null +++ b/app/javascript/bundles/User/components/SubscriptionCancel.jsx @@ -0,0 +1,72 @@ +import React, { useState } from 'react' +import Button from '@material-ui/core/Button' +import Dialog from '@material-ui/core/Dialog' +import DialogActions from '@material-ui/core/DialogActions' +import DialogContent from '@material-ui/core/DialogContent' +import DialogContentText from '@material-ui/core/DialogContentText' +import DialogTitle from '@material-ui/core/DialogTitle' + +const SUBSCRIPTION_CANCEL_URL = userID => `/users/${userID}/subscription` + +function SubscriptionCancelView ({ user, subscription }) { + const [open, setOpen] = useState(false) + + const handleClickOpen = () => { + setOpen(true) + } + + const handleClose = () => { + setOpen(false) + } + + const handleSubscriptionCancellation = async () => { + await fetch(SUBSCRIPTION_CANCEL_URL(user.id), { + method: 'delete' + }) + + handleClose() + } + + if (!subscription.active) { + return null + } + + return ( +
+ + + + Do you want to terminate your current subscription? + + + + Terminating your subscription will stop your current plan and the + benefits you receive from it. + + + + + + + +
+ ) +} + +export const SubscriptionCancel = props => diff --git a/app/javascript/bundles/User/components/UserShow.jsx b/app/javascript/bundles/User/components/UserShow.jsx index 75658476..95eb9a1e 100644 --- a/app/javascript/bundles/User/components/UserShow.jsx +++ b/app/javascript/bundles/User/components/UserShow.jsx @@ -16,24 +16,25 @@ function UserShowView ({ user, subscription, streak }) { const classes = useStyles() return ( - -

Contact Data

-

- Name: -

-

{user.name}

- -

- Email: -

-

{user.email}

- - {subscription && streak && ( + <> + {subscription && subscription.active && streak && (

You have been subscribed for {streak}

)} -
+ +

Contact Data

+

+ Name: +

+

{user.name}

+ +

+ Email: +

+

{user.email}

+
+ ) } diff --git a/app/javascript/packs/user-bundle.js b/app/javascript/packs/user-bundle.js index 84c70448..3c9708d8 100644 --- a/app/javascript/packs/user-bundle.js +++ b/app/javascript/packs/user-bundle.js @@ -2,8 +2,10 @@ import ReactOnRails from 'react-on-rails' import { UserShow } from '../bundles/User/components/UserShow' import { DonationsHistory } from '../bundles/User/components/DonationsHistory' +import { SubscriptionCancel } from '../bundles/User/components/SubscriptionCancel' ReactOnRails.register({ UserShow, - DonationsHistory + DonationsHistory, + SubscriptionCancel }) diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 4d3c934a..b27e7193 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -3,4 +3,5 @@ <%= react_component("UserShow", props: {user: @user, streak: @user.current_streak, subscription: @user.subscription}) %> <%= react_component("DonationsHistory", props: {activePlan: @user.subscription&.plan, donations: @user.donations}) %> + <%= react_component("SubscriptionCancel", props: {user: @user, subscription: @user.subscription}) %> From 7fb71a21b9733d3c7904ee68f53fd86ed76c6b23 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 14 Oct 2019 14:01:44 +0200 Subject: [PATCH 05/11] chore: run migrations --- db/schema.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/schema.rb b/db/schema.rb index 96717caa..a2d9d222 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -86,6 +86,7 @@ t.uuid "donation_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.uuid "user_id" t.index ["donation_id"], name: "index_subscription_donations_on_donation_id" t.index ["subscription_id", "donation_id"], name: "index_subscription_donations_on_subscription_id_and_donation_id", unique: true t.index ["subscription_id"], name: "index_subscription_donations_on_subscription_id" From 73265441fa3acba6b1d2edece96ec3d6b6a7dec3 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 14 Oct 2019 14:13:58 +0200 Subject: [PATCH 06/11] fix(subscription): not being active when creating and apply scope to user --- app/controllers/subscription_charges_controller.rb | 1 + app/models/user.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/subscription_charges_controller.rb b/app/controllers/subscription_charges_controller.rb index 4bab40fa..668c0cfe 100644 --- a/app/controllers/subscription_charges_controller.rb +++ b/app/controllers/subscription_charges_controller.rb @@ -34,6 +34,7 @@ def create ) if charge + @subscription.active = true @subscription.last_charge_at = DateTime.now @subscription.save diff --git a/app/models/user.rb b/app/models/user.rb index 38dd880d..a5eda363 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,7 +5,7 @@ class User < ApplicationRecord SSO_ATTRIBUTES = %w[admin avatar_url banned custom_fields email name username].freeze - has_one :subscription + has_one :subscription, -> { where active: true } has_many :cards has_many :donations From 8c17bc2bd51aae41c182d0439bd5cc20e40a7308 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 15 Oct 2019 12:19:24 +0200 Subject: [PATCH 07/11] feat: add feedback when canceling --- .../bundles/User/components/SubscriptionCancel.jsx | 13 ++++++++++--- spec/features/user/users_profile_spec.rb | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/javascript/bundles/User/components/SubscriptionCancel.jsx b/app/javascript/bundles/User/components/SubscriptionCancel.jsx index c326e486..e8956e69 100644 --- a/app/javascript/bundles/User/components/SubscriptionCancel.jsx +++ b/app/javascript/bundles/User/components/SubscriptionCancel.jsx @@ -10,6 +10,7 @@ const SUBSCRIPTION_CANCEL_URL = userID => `/users/${userID}/subscription` function SubscriptionCancelView ({ user, subscription }) { const [open, setOpen] = useState(false) + const [active, toggleActive] = useState(subscription.active) const handleClickOpen = () => { setOpen(true) @@ -20,9 +21,14 @@ function SubscriptionCancelView ({ user, subscription }) { } const handleSubscriptionCancellation = async () => { - await fetch(SUBSCRIPTION_CANCEL_URL(user.id), { - method: 'delete' - }) + const isSubscriptionCancelled = await fetch( + SUBSCRIPTION_CANCEL_URL(user.id), + { + method: 'delete' + } + ) + + toggleActive(!subscription.active) handleClose() } @@ -36,6 +42,7 @@ function SubscriptionCancelView ({ user, subscription }) { + {!active && 'No active subscription'} Date: Tue, 15 Oct 2019 12:39:55 +0200 Subject: [PATCH 08/11] refactor: reflect only 1 active subscription at a time with user/subscription model --- app/controllers/subscriptions_controller.rb | 2 +- app/jobs/find_overdue_subscriptions_job.rb | 2 +- app/models/subscription.rb | 2 +- app/models/user.rb | 10 +++++++--- app/views/users/show.html.erb | 6 +++--- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index 40cf4671..7846602d 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -8,7 +8,7 @@ class SubscriptionsController < ApplicationController def destroy head :not_authorized unless current_user == @user - subscription = @user.subscription + subscription = @user.active_subscription subscription.active = false if subscription.save diff --git a/app/jobs/find_overdue_subscriptions_job.rb b/app/jobs/find_overdue_subscriptions_job.rb index 99e1840f..c308f367 100644 --- a/app/jobs/find_overdue_subscriptions_job.rb +++ b/app/jobs/find_overdue_subscriptions_job.rb @@ -4,7 +4,7 @@ class FindOverdueSubscriptionsJob < ApplicationJob queue_as :default def perform - subscriptions_to_charge = Subscription.active.select do |subscription| + subscriptions_to_charge = Subscription.where(active: true).select do |subscription| # create charge if there's no last_charge_at date, meaning it would be the # first time we attempt to charge this customer this subscription. subscription.last_charge_at ? subscription.last_charge_at <= 30.days.ago : true diff --git a/app/models/subscription.rb b/app/models/subscription.rb index d93c4ed9..07154896 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -12,7 +12,7 @@ class Subscription < ApplicationRecord validates :plan_id, presence: true validates :user_id, uniqueness: { scope: %i[plan_id active] }, if: :user? - scope :active, -> { where(active: true) } + scope :active, -> { where(active: true).first } def user? !user_id.blank? diff --git a/app/models/user.rb b/app/models/user.rb index a5eda363..7157dd0f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,7 +5,7 @@ class User < ApplicationRecord SSO_ATTRIBUTES = %w[admin avatar_url banned custom_fields email name username].freeze - has_one :subscription, -> { where active: true } + has_many :subscriptions has_many :cards has_many :donations @@ -32,10 +32,14 @@ def admin? end def current_streak - return 'Currently, you don\'t own an active subscribption' unless subscription + return 'Currently, you don\'t own an active subscription' unless active_subscription - start_date ||= subscription.start_date + start_date ||= active_subscription.start_date time_ago_in_words(start_date) end + + def active_subscription + subscriptions.active + end end diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index b27e7193..f1cb39c4 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,7 +1,7 @@

<%= notice %>

- <%= react_component("UserShow", props: {user: @user, streak: @user.current_streak, subscription: @user.subscription}) %> - <%= react_component("DonationsHistory", props: {activePlan: @user.subscription&.plan, donations: @user.donations}) %> - <%= react_component("SubscriptionCancel", props: {user: @user, subscription: @user.subscription}) %> + <%= react_component("UserShow", props: {user: @user, streak: @user.current_streak, subscription: @user.active_subscription}) %> + <%= react_component("DonationsHistory", props: {activePlan: @user.active_subscription&.plan, donations: @user.donations}) %> + <%= react_component("SubscriptionCancel", props: {user: @user, subscription: @user.active_subscription}) %>
From 6c1e49262affdea43bc1d9a1e330f98417464cc1 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 15 Oct 2019 13:19:26 +0200 Subject: [PATCH 09/11] feat: disable ability to subscribe to same plan from homepage --- app/views/static_pages/home.html.erb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/static_pages/home.html.erb b/app/views/static_pages/home.html.erb index 23ba7b23..ce1e3728 100644 --- a/app/views/static_pages/home.html.erb +++ b/app/views/static_pages/home.html.erb @@ -39,7 +39,11 @@

<%= plan.headline %>

<%= plan.description %> -

<%= link_to 'Subscribe', @current_user ? new_subscription_charge_path(plan_id: plan) : login_path %>

+ <% if @current_user&.active_subscription&.plan.id == plan.id %> +

Subscribed

+ <% else %> +

<%= link_to 'Subscribe', @current_user ? new_subscription_charge_path(plan_id: plan) : login_path %>

+ <% end %> <% end %>
From 3f44ba04fc71ffe1b163be6d447e6d0cdc7349be Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 15 Oct 2019 13:32:28 +0200 Subject: [PATCH 10/11] fix: verification of active subscription --- app/controllers/static_pages_controller.rb | 1 + app/models/user.rb | 2 +- app/views/static_pages/home.html.erb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index fa4a5a85..65db7df3 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -3,6 +3,7 @@ class StaticPagesController < ApplicationController def home @current_user = current_user + @current_user_subscription = @current_user.active_subscription @plans = Plan.all.order('amount asc') end end diff --git a/app/models/user.rb b/app/models/user.rb index 7157dd0f..a9fd930a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,6 +40,6 @@ def current_streak end def active_subscription - subscriptions.active + subscriptions.active unless subscriptions.active.blank? end end diff --git a/app/views/static_pages/home.html.erb b/app/views/static_pages/home.html.erb index ce1e3728..5f884623 100644 --- a/app/views/static_pages/home.html.erb +++ b/app/views/static_pages/home.html.erb @@ -39,7 +39,7 @@

<%= plan.headline %>

<%= plan.description %> - <% if @current_user&.active_subscription&.plan.id == plan.id %> + <% if @current_user_subscription&.plan&.id == plan.id %>

Subscribed

<% else %>

<%= link_to 'Subscribe', @current_user ? new_subscription_charge_path(plan_id: plan) : login_path %>

From baf39510c82304178d830bee026934f27f5bb295 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Wed, 16 Oct 2019 19:04:51 +0200 Subject: [PATCH 11/11] fix: user without subscription cannot see homescreen --- app/controllers/static_pages_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index 65db7df3..d7dbddf5 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -3,7 +3,7 @@ class StaticPagesController < ApplicationController def home @current_user = current_user - @current_user_subscription = @current_user.active_subscription + @current_user_subscription = @current_user&.active_subscription @plans = Plan.all.order('amount asc') end end