Commit 8e48ac5b authored by Jay Swain's avatar Jay Swain

Gitlab.com has a subscription expired banner

.com users are informed that their subscription is going to expire, or
is expired, and provided an easy path to update/renew their
subscription.

We've previously had a similar message for self-managed users, this
commit brings the message over to .com

* only display to expired accounts for 30 days

part of: https://gitlab.com/gitlab-org/growth/product/-/issues/102
parent 85f4e696
......@@ -5,7 +5,7 @@
.mobile-overlay
.alert-wrapper
= render 'shared/outdated_browser'
= render_if_exists "layouts/header/ee_license_banner"
= render_if_exists "layouts/header/ee_subscribable_banner"
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/nav/classification_level_banner"
......
......@@ -8,4 +8,5 @@
- unless project.empty_repo?
= render 'shared/auto_devops_implicitly_enabled_banner', project: project
= render_if_exists 'projects/above_size_limit_warning', project: project
= render_if_exists "layouts/header/ee_subscribable_banner", subscription: true
= render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)]
......@@ -180,6 +180,21 @@ module EE
"The total size of this project's repository #{show_lfs} will be limited to this size. 0 for unlimited. Leave empty to inherit the group/global value."
end
def subscription_message
return unless ::Gitlab.com?
::Gitlab::ExpiringSubscriptionMessage.new(
subscribable: decorated_subscription,
signed_in: signed_in?,
is_admin: can?(current_user, :developer_access, @project),
namespace: @project.namespace
).message
end
def decorated_subscription
SubscriptionPresenter.new(@project.gitlab_subscription)
end
override :membership_locked?
def membership_locked?
group = @project.group
......
......@@ -19,16 +19,11 @@ module LicenseHelper
end
def license_message(signed_in: signed_in?, is_admin: current_user&.admin?)
return unless current_license
return unless signed_in
return unless (is_admin && current_license.notify_admins?) || current_license.notify_users?
message = []
message << license_message_subject
message << expiration_blocking_message
message.reject {|string| string.blank? }.join(' ').html_safe
Gitlab::ExpiringSubscriptionMessage.new(
subscribable: current_license,
signed_in: signed_in,
is_admin: is_admin
).message
end
def seats_calculation_message
......@@ -105,44 +100,4 @@ module LicenseHelper
def active_user_count
User.active.count
end
def license_message_subject
if current_license.expired?
message = if current_license.block_changes?
_('Your subscription has been downgraded')
else
_('Your subscription expired!')
end
else
remaining_days = pluralize(current_license.remaining_days, 'day')
message = _('Your subscription will expire in %{remaining_days}') % { remaining_days: remaining_days }
end
message = content_tag(:strong, message)
content_tag(:p, message, class: 'mb-2')
end
def expiration_blocking_message
return '' unless current_license.will_block_changes?
plan_name = current_license.plan.titleize
strong = "<strong>".html_safe
strong_close = "</strong>".html_safe
if current_license.expired?
if current_license.block_changes?
message = _('You didn\'t renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan.') % { plan_name: plan_name, strong: strong, strong_close: strong_close }
else
remaining_days = pluralize((current_license.block_changes_at - Date.today).to_i, 'day')
message = _('No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} to renew your subscription.') % { plan_name: plan_name, remaining_days: remaining_days, strong: strong, strong_close: strong_close }
end
else
message = _('Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features.') % { expires_on: current_license.expires_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close }
end
content_tag(:p, message.html_safe)
end
end
......@@ -159,6 +159,7 @@ module EE
delegate :merge_pipelines_enabled, :merge_pipelines_enabled=, :merge_pipelines_enabled?, :merge_pipelines_were_disabled?, to: :ci_cd_settings
delegate :merge_trains_enabled?, to: :ci_cd_settings
delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
delegate :gitlab_subscription, to: :namespace
validates :repository_size_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
......
# frozen_string_literal: true
class SubscriptionPresenter < Gitlab::View::Presenter::Delegated
presents :subscription
def block_changes?
expired?
end
def plan
namespace.try(:actual_plan_name)
end
def notify_admins?
remaining_days && remaining_days < 30
end
def notify_users?
false
end
def expires_at
end_date
end
alias_method :block_changes_at, :expires_at
def remaining_days
return unless end_date
(end_date - Date.today).to_i
end
def will_block_changes?
true
end
end
- if license_message.present?
- if local_assigns[:subscription]
- subscribable = decorated_subscription
- message = subscription_message
- else
- subscribable = current_license
- message = license_message
- if message.present?
.container-fluid.container-limited.pt-3
.alert.alert-dismissible.gitlab-ee-license-banner.hidden.js-gitlab-ee-license-banner.pb-5.border-width-1px.border-style-solid.border-color-default.border-radius-default{ role: 'alert', data: { license_expiry: current_license.expires_at } }
.alert.alert-dismissible.gitlab-ee-license-banner.hidden.js-gitlab-ee-license-banner.pb-5.border-width-1px.border-style-solid.border-color-default.border-radius-default{ role: 'alert', data: { license_expiry: subscribable.expires_at } }
%button.close.p-2{ type: 'button', 'data-dismiss' => 'alert', 'aria-label' => 'Dismiss banner' }
%span{ 'aria-hidden' => 'true' }
= sprite_icon('merge-request-close-m', size: 24)
.d-flex.flex-row
.pr-4.pl-3.pt-2
- if current_license.expired?
- if current_license.block_changes?
- if subscribable.expired?
- if subscribable.block_changes?
= image_tag('illustrations/subscription-downgraded.svg')
- else
= image_tag('illustrations/subscription-cancelled.svg')
- else
= image_tag('illustrations/subscription-warning.svg')
.text-left.pt-2
= license_message
= message
- if current_license.block_changes?
- if subscribable.block_changes?
= link_to 'Upgrade your plan', 'https://customers.gitlab.com/subscriptions/my_renewal', class: 'btn btn-primary'
- else
= link_to 'Renew subscription', 'https://customers.gitlab.com/subscriptions/my_renewal', class: 'btn btn-primary'
......
---
title: ".com has a subscription expired banner"
merge_request: 28238
author:
type: added
# frozen_string_literal: true
module Gitlab
class ExpiringSubscriptionMessage
include ActionView::Helpers::TextHelper
attr_reader :subscribable, :signed_in, :is_admin, :namespace
def initialize(subscribable:, signed_in:, is_admin:, namespace: nil)
@subscribable = subscribable
@signed_in = signed_in
@is_admin = is_admin
@namespace = namespace
end
def message
return unless notifiable?
message = []
message << license_message_subject
message << expiration_blocking_message
message.reject { |string| string.blank? }.join(' ').html_safe
end
private
def license_message_subject
message = subscribable.expired? ? expired_subject : expiring_subject
message = content_tag(:strong, message)
content_tag(:p, message, class: 'mb-2')
end
def expired_subject
if subscribable.block_changes?
_('Your subscription has been downgraded')
else
_('Your subscription expired!')
end
end
def expiring_subject
remaining_days = pluralize(subscribable.remaining_days, 'day')
_('Your subscription will expire in %{remaining_days}') % { remaining_days: remaining_days }
end
def expiration_blocking_message
return '' unless subscribable.will_block_changes?
message = subscribable.expired? ? expired_message : expiring_message
content_tag(:p, message.html_safe)
end
def expired_message
if subscribable.block_changes?
if namespace
_('You didn\'t renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan.') % { plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name }
else
_('You didn\'t renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan.') % { plan_name: plan_name, strong: strong, strong_close: strong_close }
end
else
remaining_days = pluralize((subscribable.block_changes_at - Date.today).to_i, 'day')
_('No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} to renew your subscription.') % { plan_name: plan_name, remaining_days: remaining_days, strong: strong, strong_close: strong_close }
end
end
def expiring_message
if namespace
_('Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features.') % { expires_on: subscribable.expires_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close, namespace_name: namespace.name }
else
_('Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features.') % { expires_on: subscribable.expires_at.strftime("%Y-%m-%d"), plan_name: plan_name, strong: strong, strong_close: strong_close }
end
end
def notifiable?
subscribable &&
signed_in &&
((is_admin && subscribable.notify_admins?) || subscribable.notify_users?) &&
expired_subscribable_within_notification_window?
end
def expired_subscribable_within_notification_window?
return true unless subscribable.expired?
expired_at = subscribable.expires_at
(expired_at..(expired_at + 30.days)).cover?(Date.today)
end
def plan_name
subscribable.plan.titleize
end
def strong
"<strong>".html_safe
end
def strong_close
"</strong>".html_safe
end
end
end
......@@ -9,110 +9,23 @@ describe LicenseHelper do
end
describe '#license_message' do
subject { license_message(signed_in: signed_in, is_admin: is_admin) }
context 'license installed' do
let(:license) { double(:license) }
let(:expired_date) { Time.utc(2020, 3, 9, 10) }
let(:today) { Time.utc(2020, 3, 7, 10) }
let(:message_mock) { double(:message_mock) }
before do
allow(License).to receive(:current).and_return(license)
allow(license).to receive(:plan).and_return('ultimate')
allow(license).to receive(:expires_at).and_return(expired_date)
end
context 'license is notify admins' do
before do
allow(license).to receive(:notify_admins?).and_return(true)
end
context 'admin signed in' do
let(:signed_in) { true }
let(:is_admin) { true }
context 'license expired' do
let(:expired_date) { Time.utc(2020, 3, 9).to_date }
before do
allow(license).to receive(:expired?).and_return(true)
allow(license).to receive(:expires_at).and_return(expired_date)
end
context 'and it will block changes when it expires' do
before do
allow(license).to receive(:will_block_changes?).and_return(true)
end
context 'and its currently blocking changes' do
before do
allow(license).to receive(:block_changes?).and_return(true)
allow(license).to receive(:block_changes_at).and_return(expired_date)
end
it 'has a nice subject' do
allow(license).to receive(:will_block_changes?).and_return(false)
expect(subject).to have_text('Your subscription has been downgraded')
end
it 'has an expiration blocking message' do
Timecop.freeze(today) do
expect(subject).to have_text("You didn't renew your Ultimate subscription so it was downgraded to the GitLab Core Plan")
end
end
end
context 'and its NOT currently blocking changes' do
before do
allow(license).to receive(:block_changes?).and_return(false)
end
it 'has a nice subject' do
allow(license).to receive(:will_block_changes?).and_return(false)
expect(subject).to have_text('Your subscription expired!')
end
it 'has an expiration blocking message' do
allow(license).to receive(:block_changes_at).and_return(expired_date)
Timecop.freeze(today) do
expect(subject).to have_text('No worries, you can still use all the Ultimate features for now. You have 2 days to renew your subscription.')
end
end
end
end
end
context 'license NOT expired' do
before do
allow(license).to receive(:expired?).and_return(false)
allow(license).to receive(:remaining_days).and_return(4)
allow(license).to receive(:will_block_changes?).and_return(true)
allow(license).to receive(:block_changes_at).and_return(expired_date)
end
it 'has a nice subject' do
expect(subject).to have_text('Your subscription will expire in 4 days')
end
it 'has an expiration blocking message' do
Timecop.freeze(today) do
expect(subject).to have_text('Your Ultimate subscription will expire on 2020-03-09. After that, you will not to be able to create issues or merge requests as well as many other features.')
end
end
end
end
end
end
it 'calls another class with args' do
expect(Gitlab::ExpiringSubscriptionMessage).to receive(:new).with(
subscribable: license,
signed_in: true,
is_admin: false
).and_return(message_mock)
context 'no license installed' do
let(:license) { nil }
let(:signed_in) { true }
let(:is_admin) { true }
expect(message_mock).to receive(:message)
it { is_expected.to be_blank }
license_message(signed_in: true, is_admin: false)
end
end
......
......@@ -222,4 +222,36 @@ describe ProjectsHelper do
end
end
end
describe '#subscription_message' do
let(:gitlab_subscription) { double(:gitlab_subscription) }
let(:decorated_mock) { double(:decorated_mock) }
let(:message_mock) { double(:message_mock) }
let(:user) { double(:user_mock) }
it 'if it is not Gitlab.com? it returns nil' do
allow(Gitlab).to receive(:com?).and_return(false)
expect(helper.subscription_message).to be_nil
end
it 'calls 2 classes if is Gitlab.com?' do
allow(Gitlab).to receive(:com?).and_return(true)
allow(helper).to receive(:signed_in?).and_return(true)
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :developer_access, project).and_return(true)
allow(project).to receive(:gitlab_subscription).and_return(gitlab_subscription)
expect(SubscriptionPresenter).to receive(:new).with(gitlab_subscription).and_return(decorated_mock)
expect(::Gitlab::ExpiringSubscriptionMessage).to receive(:new).with(
subscribable: decorated_mock,
signed_in: true,
is_admin: true,
namespace: project.namespace
).and_return(message_mock)
expect(message_mock).to receive(:message).and_return('hey yay yay yay')
expect(helper.subscription_message).to eq('hey yay yay yay')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ExpiringSubscriptionMessage do
include ActionView::Helpers::SanitizeHelper
describe 'message' do
subject { strip_tags(message) }
let(:subscribable) { double(:license) }
let(:namespace) { nil }
let(:message) do
described_class.new(
subscribable: subscribable,
signed_in: true,
is_admin: true,
namespace: namespace
).message
end
context 'subscribable installed' do
let(:expired_date) { Time.utc(2020, 3, 9, 10) }
let(:today) { Time.utc(2020, 3, 7, 10) }
before do
allow(subscribable).to receive(:plan).and_return('ultimate')
allow(subscribable).to receive(:expires_at).and_return(expired_date)
end
context 'subscribable should not notify admins' do
it 'returns nil' do
allow(subscribable).to receive(:notify_admins?).and_return(false)
allow(subscribable).to receive(:notify_users?).and_return(false)
expect(subject).to be nil
end
end
context 'subscribable should notify admins' do
before do
allow(subscribable).to receive(:notify_admins?).and_return(true)
end
context 'admin signed in' do
let(:signed_in) { true }
let(:is_admin) { true }
context 'subscribable expired' do
let(:expired_date) { Time.utc(2020, 3, 1, 10).to_date }
before do
allow(subscribable).to receive(:expired?).and_return(true)
allow(subscribable).to receive(:expires_at).and_return(expired_date)
end
context 'when it blocks changes' do
before do
allow(subscribable).to receive(:will_block_changes?).and_return(true)
end
context 'when it is currently blocking changes' do
before do
allow(subscribable).to receive(:block_changes?).and_return(true)
allow(subscribable).to receive(:block_changes_at).and_return(expired_date)
end
it 'has a nice subject' do
allow(subscribable).to receive(:will_block_changes?).and_return(false)
Timecop.freeze(today) do
expect(subject).to include('Your subscription has been downgraded')
end
end
context 'no namespace' do
it 'has an expiration blocking message' do
Timecop.freeze(today) do
expect(subject).to include("You didn't renew your Ultimate subscription so it was downgraded to the GitLab Core Plan")
end
end
end
context 'with namespace' do
let(:namespace) { double(:namespace, name: 'No Limit Records') }
it 'has an expiration blocking message' do
Timecop.freeze(today) do
expect(subject).to include("You didn't renew your Ultimate subscription for No Limit Records so it was downgraded to the free plan")
end
end
end
end
context 'when it is not currently blocking changes' do
before do
allow(subscribable).to receive(:block_changes?).and_return(false)
end
it 'has a nice subject' do
allow(subscribable).to receive(:will_block_changes?).and_return(false)
Timecop.freeze(today) do
expect(subject).to include('Your subscription expired!')
end
end
it 'has an expiration blocking message' do
allow(subscribable).to receive(:block_changes_at).and_return(Time.utc(2020, 3, 9, 10).to_date)
Timecop.freeze(today) do
expect(subject).to include('No worries, you can still use all the Ultimate features for now. You have 2 days to renew your subscription.')
end
end
end
end
end
context 'subscribable is expiring soon' do
before do
allow(subscribable).to receive(:expired?).and_return(false)
allow(subscribable).to receive(:remaining_days).and_return(4)
allow(subscribable).to receive(:will_block_changes?).and_return(true)
allow(subscribable).to receive(:block_changes_at).and_return(expired_date)
end
it 'has a nice subject' do
Timecop.freeze(today) do
expect(subject).to include('Your subscription will expire in 4 days')
end
end
context 'without namespace' do
it 'has an expiration blocking message' do
Timecop.freeze(today) do
expect(subject).to include('Your Ultimate subscription will expire on 2020-03-09. After that, you will not to be able to create issues or merge requests as well as many other features.')
end
end
end
context 'with namespace' do
let(:namespace) { double(:namespace, name: 'No Limit Records') }
it 'has an expiration blocking message' do
Timecop.freeze(today) do
expect(subject).to include('Your Ultimate subscription for No Limit Records will expire on 2020-03-09. After that, you will not to be able to create issues or merge requests as well as many other features.')
end
end
end
end
end
end
end
context 'no subscribable installed' do
let(:subscribable) { nil }
it { is_expected.to be_blank }
end
end
end
......@@ -2608,4 +2608,23 @@ describe Project do
end
end
end
describe '#gitlab_subscription' do
subject { project.gitlab_subscription }
let(:project) { create(:project, namespace: namespace) }
context 'has a gitlab subscription' do
let(:namespace) { subscription.namespace }
let(:subscription) { create(:gitlab_subscription) }
it { is_expected.to eq(subscription) }
end
context 'does not have a gitlab subscription' do
let(:namespace) { create(:namespace) }
it { is_expected.to be_nil }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe SubscriptionPresenter do
let(:subscription) { create(:gitlab_subscription) }
let(:presenter) { described_class.new(subscription, {}) }
describe '#plan' do
subject { presenter.plan }
it { is_expected.to eq('gold') }
end
describe '#notify_admins?' do
subject { presenter.notify_admins? }
let(:today) { Time.utc(2020, 3, 7, 10) }
it 'is false when remaining days is nil' do
expect(subject).to be false
end
it 'remaining days more than 30 is false' do
allow(subscription).to receive(:end_date).and_return(Time.utc(2020, 4, 9, 10).to_date)
Timecop.freeze(today) do
expect(subject).to be false
end
end
it 'remaining days less than 30 is true' do
allow(subscription).to receive(:end_date).and_return(Time.utc(2020, 3, 9, 10).to_date)
Timecop.freeze(today) do
expect(subject).to be true
end
end
end
describe '#notify_users?' do
subject { presenter.notify_users? }
it { is_expected.to be false }
end
describe '#block_changes_at' do
subject { presenter.block_changes_at }
it { is_expected.to eq(subscription.end_date) }
end
describe '#block_changes?' do
subject { presenter.block_changes? }
it { is_expected.to be false }
context 'is expired' do
before do
allow(subscription).to receive(:expired?).and_return(true)
end
it { is_expected.to be true }
end
end
describe '#will_block_changes?' do
subject { presenter.will_block_changes? }
it { is_expected.to be true }
end
describe '#remaining_days' do
subject { presenter.remaining_days }
let(:today) { Time.utc(2020, 3, 7, 10) }
it 'is nil when end_date is nil' do
allow(subscription).to receive(:end_date).and_return(nil)
expect(subject).to be nil
end
it 'returns the number of days between end_date and today' do
allow(subscription).to receive(:end_date).and_return(Time.utc(2020, 3, 9, 10).to_date)
Timecop.freeze(today) do
expect(subject).to eq(2)
end
end
end
end
......@@ -23259,6 +23259,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
......@@ -23484,6 +23487,9 @@ msgstr ""
msgid "YouTube"
msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment