mirror of
https://github.com/plausible/analytics.git
synced 2024-09-11 18:07:33 +03:00
Limit grandfathering to *active* subscribers and trials (#3524)
* refactor asserting plan generation in plans_test.exs * stop grandfathering old expired trials For users who registered before the business tiers release, we want to offer a chance to subscribe to a grandfathered plan. However, if they let their trial expire and don't subscribe in the next 10 days, they'll lose that opportunity. * stop grandfathering expired subscriptions * remove default title and icon from Generic.notice * fix bug with dismissable notice classList is null when dismissable_id is not given * alias Plausible.Auth.User * Refactor Generic.notice component Make it easy to apply different colors * move subscription_cancelled_notice across the app And remove from user settings > subscription box. Also, include a note about losing grandfathered status when letting the subscription expire. * allow full width in Generic.notice * use Generic.notice for subscription_past_due_notice * use Generic.notice for subscription_paused_notice * prevent two notices clashing into each other with gap-y-2 * define attrs for phx components * optimize for light mode * make subscription cancelled notice dismissable but if it's dismiss, show it in the place where it was before in the account settings > subscription box * make function private * replace function doc with regular comment to avoid compile warning * use array for classnames Co-authored-by: Vinicius Brasil <vini@hey.com> * fix typos in function doc --------- Co-authored-by: Vinicius Brasil <vini@hey.com>
This commit is contained in:
parent
13055aafc0
commit
d66322e12d
@ -281,6 +281,10 @@ defmodule Plausible.Billing do
|
||||
|
||||
def paddle_api(), do: Application.fetch_env!(:plausible, :paddle_api)
|
||||
|
||||
def cancelled_subscription_notice_dismiss_id(%Plausible.Auth.User{} = user) do
|
||||
"subscription_cancelled__#{user.id}"
|
||||
end
|
||||
|
||||
defp active_subscription_query(user_id) do
|
||||
from(s in Subscription,
|
||||
where: s.user_id == ^user_id and s.status == ^Subscription.Status.active(),
|
||||
|
@ -38,16 +38,17 @@ defmodule Plausible.Billing.Plans do
|
||||
still choose from old plans.
|
||||
"""
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
|
||||
def growth_plans_for(%User{} = user) do
|
||||
def growth_plans_for(%User{} = user, now \\ Timex.now()) do
|
||||
user = Plausible.Users.with_subscription(user)
|
||||
v4_available = FunWithFlags.enabled?(:business_tier, for: user)
|
||||
owned_plan = get_regular_plan(user.subscription)
|
||||
|
||||
cond do
|
||||
Application.get_env(:plausible, :environment) == "dev" -> @sandbox_plans
|
||||
is_nil(owned_plan) && Timex.before?(user.inserted_at, @business_tier_launch) -> @plans_v3
|
||||
is_nil(owned_plan) && grandfathered_trial?(user.trial_expiry_date, now) -> @plans_v3
|
||||
is_nil(owned_plan) && v4_available -> @plans_v4
|
||||
is_nil(owned_plan) -> @plans_v3
|
||||
user.subscription && Subscriptions.expired?(user.subscription) -> @plans_v4
|
||||
owned_plan.kind == :business -> @plans_v4
|
||||
owned_plan.generation == 1 -> @plans_v1
|
||||
owned_plan.generation == 2 -> @plans_v2
|
||||
@ -57,19 +58,34 @@ defmodule Plausible.Billing.Plans do
|
||||
|> Enum.filter(&(&1.kind == :growth))
|
||||
end
|
||||
|
||||
def business_plans_for(%User{} = user) do
|
||||
def business_plans_for(%User{} = user, now \\ Timex.now()) do
|
||||
user = Plausible.Users.with_subscription(user)
|
||||
owned_plan = get_regular_plan(user.subscription)
|
||||
|
||||
cond do
|
||||
Application.get_env(:plausible, :environment) == "dev" -> @sandbox_plans
|
||||
is_nil(owned_plan) && Timex.before?(user.inserted_at, @business_tier_launch) -> @plans_v3
|
||||
is_nil(owned_plan) && grandfathered_trial?(user.trial_expiry_date, now) -> @plans_v3
|
||||
user.subscription && Subscriptions.expired?(user.subscription) -> @plans_v4
|
||||
owned_plan && owned_plan.generation < 4 -> @plans_v3
|
||||
true -> @plans_v4
|
||||
end
|
||||
|> Enum.filter(&(&1.kind == :business))
|
||||
end
|
||||
|
||||
# Takes a Date struct argument representing the trial end date of a user.
|
||||
# If the `trial_expiry` is `nil`, it means that the user has not started
|
||||
# their trial yet (i.e. invited user), and this function returns false.
|
||||
defp grandfathered_trial?(nil, _now), do: false
|
||||
|
||||
defp grandfathered_trial?(trial_expiry, now) do
|
||||
trial_start = Timex.shift(trial_expiry, days: -30)
|
||||
|
||||
joined_before_business_tiers = Timex.before?(trial_start, @business_tier_launch)
|
||||
trial_active_or_expired_less_than_10d_ago = Timex.diff(now, trial_expiry, :days) <= 10
|
||||
|
||||
joined_before_business_tiers && trial_active_or_expired_less_than_10d_ago
|
||||
end
|
||||
|
||||
def available_plans_for(%User{} = user, opts \\ []) do
|
||||
plans = growth_plans_for(user) ++ business_plans_for(user)
|
||||
|
||||
|
@ -4,13 +4,14 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
use Phoenix.Component
|
||||
import PlausibleWeb.Components.Generic
|
||||
require Plausible.Billing.Subscription.Status
|
||||
alias Plausible.Auth.User
|
||||
alias Plausible.Billing.Feature.{RevenueGoals, Funnels}
|
||||
alias Plausible.Billing.Feature.{Props, StatsAPI}
|
||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||
alias Plausible.Billing.{Subscription, Plans, Plan, Subscriptions}
|
||||
|
||||
attr(:billable_user, Plausible.Auth.User, required: true)
|
||||
attr(:current_user, Plausible.Auth.User, required: true)
|
||||
attr(:billable_user, User, required: true)
|
||||
attr(:current_user, User, required: true)
|
||||
attr(:feature_mod, :atom, required: true, values: Plausible.Billing.Feature.list())
|
||||
attr(:grandfathered?, :boolean, default: false)
|
||||
attr(:size, :atom, default: :sm)
|
||||
@ -33,7 +34,7 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
|
||||
not has_access? ->
|
||||
~H"""
|
||||
<.notice class="rounded-t-md rounded-b-none" size={@size} {@rest}>
|
||||
<.notice class="rounded-t-md rounded-b-none" size={@size} {@rest} title="Notice">
|
||||
<%= account_label(@current_user, @billable_user) %> does not have access to <%= assigns.feature_mod.display_name() %>. To get access to this feature,
|
||||
<.upgrade_call_to_action current_user={@current_user} billable_user={@billable_user} />.
|
||||
</.notice>
|
||||
@ -56,8 +57,8 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
end
|
||||
end
|
||||
|
||||
attr(:billable_user, Plausible.Auth.User, required: true)
|
||||
attr(:current_user, Plausible.Auth.User, required: true)
|
||||
attr(:billable_user, User, required: true)
|
||||
attr(:current_user, User, required: true)
|
||||
attr(:limit, :integer, required: true)
|
||||
attr(:resource, :string, required: true)
|
||||
attr(:rest, :global)
|
||||
@ -65,7 +66,7 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
|
||||
def limit_exceeded_notice(assigns) do
|
||||
~H"""
|
||||
<.notice {@rest}>
|
||||
<.notice {@rest} title="Notice">
|
||||
<%= account_label(@current_user, @billable_user) %> is limited to <%= @limit %> <%= @resource %>. To increase this limit,
|
||||
<.upgrade_call_to_action current_user={@current_user} billable_user={@billable_user} />.
|
||||
</.notice>
|
||||
@ -215,86 +216,120 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:user, :map, required: true)
|
||||
attr(:dismissable, :boolean, default: true)
|
||||
|
||||
@doc """
|
||||
Given a user with a cancelled subscription, this component renders a cancelled
|
||||
subscription notice. If the given user does not have a subscription or it has a
|
||||
different status, this function returns an empty template.
|
||||
|
||||
It also takes a dismissable argument which renders the notice dismissable (with
|
||||
the help of JavaScript and localStorage). We show a dismissable notice about a
|
||||
cancelled subscription across the app, but when the user dismisses it, we will
|
||||
start displaying it in the account settings > subscription section instead.
|
||||
|
||||
So it's either shown across the app, or only on the /settings page. Depending
|
||||
on whether the localStorage flag to dismiss it has been set or not.
|
||||
"""
|
||||
def subscription_cancelled_notice(assigns)
|
||||
|
||||
def subscription_cancelled_notice(
|
||||
%{
|
||||
dismissable: true,
|
||||
user: %User{subscription: %Subscription{status: Subscription.Status.deleted()}}
|
||||
} =
|
||||
assigns
|
||||
) do
|
||||
~H"""
|
||||
<aside id="global-subscription-cancelled-notice" class="container">
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
dismissable_id={Plausible.Billing.cancelled_subscription_notice_dismiss_id(@user)}
|
||||
title="Subscription cancelled"
|
||||
theme={:red}
|
||||
class="shadow-md dark:shadow-none"
|
||||
>
|
||||
<.subscription_cancelled_notice_body user={@user} />
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</aside>
|
||||
"""
|
||||
end
|
||||
|
||||
def subscription_cancelled_notice(
|
||||
%{
|
||||
dismissable: false,
|
||||
user: %User{subscription: %Subscription{status: Subscription.Status.deleted()}}
|
||||
} =
|
||||
assigns
|
||||
) do
|
||||
assigns = assign(assigns, :container_id, "local-subscription-cancelled-notice")
|
||||
|
||||
~H"""
|
||||
<aside id={@container_id} class="hidden">
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
title="Subscription cancelled"
|
||||
theme={:red}
|
||||
class="shadow-md dark:shadow-none"
|
||||
>
|
||||
<.subscription_cancelled_notice_body user={@user} />
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</aside>
|
||||
<script
|
||||
data-localstorage-key={"notice_dismissed__#{Plausible.Billing.cancelled_subscription_notice_dismiss_id(assigns.user)}"}
|
||||
data-container-id={@container_id}
|
||||
>
|
||||
const dataset = document.currentScript.dataset
|
||||
|
||||
if (localStorage[dataset.localstorageKey]) {
|
||||
document.getElementById(dataset.containerId).classList.remove('hidden')
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
end
|
||||
|
||||
def subscription_cancelled_notice(assigns), do: ~H""
|
||||
|
||||
attr(:class, :string, default: "")
|
||||
attr(:subscription, :any, default: nil)
|
||||
|
||||
def subscription_past_due_notice(
|
||||
%{subscription: %Subscription{status: Subscription.Status.past_due()}} = assigns
|
||||
) do
|
||||
~H"""
|
||||
<aside class={@class}>
|
||||
<div class="shadow-md dark:shadow-none rounded-lg bg-yellow-100 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="w-5 h-5 mt-0.5 text-yellow-800"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 9V11M12 15H12.01M5.07183 19H18.9282C20.4678 19 21.4301 17.3333 20.6603 16L13.7321 4C12.9623 2.66667 11.0378 2.66667 10.268 4L3.33978 16C2.56998 17.3333 3.53223 19 5.07183 19Z"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p class="text-yellow-700">
|
||||
There was a problem with your latest payment. Please update your payment information to keep using Plausible.
|
||||
</p>
|
||||
<.link
|
||||
href={@subscription.update_url}
|
||||
class="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600"
|
||||
>
|
||||
Update billing info <span aria-hidden="true"> →</span>
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
title="Payment failed"
|
||||
class="shadow-md dark:shadow-none"
|
||||
>
|
||||
There was a problem with your latest payment. Please update your payment information to keep using Plausible.<.link
|
||||
href={@subscription.update_url}
|
||||
class="whitespace-nowrap font-semibold"
|
||||
> Update billing info <span aria-hidden="true"> →</span></.link>
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</aside>
|
||||
"""
|
||||
end
|
||||
|
||||
def subscription_past_due_notice(assigns), do: ~H""
|
||||
|
||||
attr(:class, :string, default: "")
|
||||
attr(:subscription, :any, default: nil)
|
||||
|
||||
def subscription_paused_notice(
|
||||
%{subscription: %Subscription{status: Subscription.Status.paused()}} = assigns
|
||||
) do
|
||||
~H"""
|
||||
<aside class={@class}>
|
||||
<div class="shadow-md dark:shadow-none rounded-lg bg-red-100 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg
|
||||
class="w-5 h-5 mt-0.5 text-yellow-800"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 9V11M12 15H12.01M5.07183 19H18.9282C20.4678 19 21.4301 17.3333 20.6603 16L13.7321 4C12.9623 2.66667 11.0378 2.66667 10.268 4L3.33978 16C2.56998 17.3333 3.53223 19 5.07183 19Z"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p class="text-red-700">
|
||||
Your subscription is paused due to failed payments. Please provide valid payment details to keep using Plausible.
|
||||
</p>
|
||||
<.link
|
||||
href={@subscription.update_url}
|
||||
class="whitespace-nowrap font-medium text-red-700 hover:text-red-600"
|
||||
>
|
||||
Update billing info <span aria-hidden="true"> →</span>
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Generic.notice
|
||||
title="Subscription paused"
|
||||
theme={:red}
|
||||
class="shadow-md dark:shadow-none"
|
||||
>
|
||||
Your subscription is paused due to failed payments. Please provide valid payment details to keep using Plausible.<.link
|
||||
href={@subscription.update_url}
|
||||
class="whitespace-nowrap font-semibold"
|
||||
> Update billing info <span aria-hidden="true"> →</span></.link>
|
||||
</PlausibleWeb.Components.Generic.notice>
|
||||
</aside>
|
||||
"""
|
||||
end
|
||||
@ -322,10 +357,11 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
~H"""
|
||||
<div
|
||||
:if={FunWithFlags.enabled?(:premium_features_private_preview) && @features_to_lose != []}
|
||||
class="container mt-2"
|
||||
class="container"
|
||||
>
|
||||
<.notice
|
||||
class="shadow-md dark:shadow-none"
|
||||
title="Notice"
|
||||
dismissable_id={"premium_features_private_preview_end__#{@user.id}"}
|
||||
>
|
||||
Business plans are now live! The private preview of <%= PlausibleWeb.TextHelpers.pretty_join(
|
||||
@ -432,6 +468,47 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
"""
|
||||
end
|
||||
|
||||
defp subscription_cancelled_notice_body(assigns) do
|
||||
if Plausible.Billing.Subscriptions.expired?(assigns.user.subscription) do
|
||||
~H"""
|
||||
<.link class="underline inline-block" href={Plausible.Billing.upgrade_route_for(@user)}>
|
||||
Upgrade your subscription
|
||||
</.link>
|
||||
<p>to get access to your stats again.</p>
|
||||
"""
|
||||
else
|
||||
~H"""
|
||||
<p>
|
||||
You have access to your stats until <span class="font-semibold inline"><%= Timex.format!(@user.subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %></span>.
|
||||
<.link class="underline inline-block" href={Plausible.Billing.upgrade_route_for(@user)}>
|
||||
Upgrade your subscription
|
||||
</.link>
|
||||
to make sure you don't lose access.
|
||||
</p>
|
||||
<.lose_grandfathering_warning user={@user} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
defp lose_grandfathering_warning(%{user: %{subscription: subscription} = user} = assigns) do
|
||||
business_tiers_available? = FunWithFlags.enabled?(:business_tier, for: user)
|
||||
plan = Plans.get_regular_plan(subscription, only_non_expired: true)
|
||||
loses_grandfathering = business_tiers_available? && plan && plan.generation < 4
|
||||
|
||||
assigns = assign(assigns, :loses_grandfathering, loses_grandfathering)
|
||||
|
||||
~H"""
|
||||
<p :if={@loses_grandfathering} class="mt-2">
|
||||
Please also note that by letting your subscription expire, you lose access to our grandfathered terms. If you want to subscribe again after that, your account will be offered the <.link
|
||||
href="https://plausible.io/#pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline"
|
||||
>latest pricing</.link>.
|
||||
</p>
|
||||
"""
|
||||
end
|
||||
|
||||
defp change_plan_or_upgrade_text(nil), do: "Upgrade"
|
||||
|
||||
defp change_plan_or_upgrade_text(%Subscription{status: Subscription.Status.deleted()}),
|
||||
|
@ -4,6 +4,21 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
"""
|
||||
use Phoenix.Component
|
||||
|
||||
@notice_themes %{
|
||||
yellow: %{
|
||||
bg: "bg-yellow-50 dark:bg-yellow-100",
|
||||
icon: "text-yellow-400",
|
||||
title_text: "text-yellow-800 dark:text-yellow-900",
|
||||
body_text: "text-yellow-700 dark:text-yellow-800"
|
||||
},
|
||||
red: %{
|
||||
bg: "bg-red-100",
|
||||
icon: "text-red-700",
|
||||
title_text: "text-red-800 dark:text-red-900",
|
||||
body_text: "text-red-700 dark:text-red-800"
|
||||
}
|
||||
}
|
||||
|
||||
attr(:type, :string, default: "button")
|
||||
attr(:class, :string, default: "")
|
||||
attr(:disabled, :boolean, default: false)
|
||||
@ -58,28 +73,31 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:title, :string, default: "Notice")
|
||||
attr(:title, :any, default: nil)
|
||||
attr(:size, :atom, default: :sm)
|
||||
attr(:theme, :atom, default: :yellow)
|
||||
attr(:dismissable_id, :any, default: nil)
|
||||
attr(:class, :string, default: "")
|
||||
attr(:rest, :global)
|
||||
slot(:inner_block)
|
||||
|
||||
def notice(assigns) do
|
||||
assigns = assign(assigns, :theme, Map.fetch!(@notice_themes, assigns.theme))
|
||||
|
||||
~H"""
|
||||
<div id={@dismissable_id} class={@dismissable_id && "hidden"}>
|
||||
<div class={"rounded-md bg-yellow-50 dark:bg-yellow-100 p-4 relative #{@class}"} {@rest}>
|
||||
<div class={["rounded-md p-4 relative", @theme.bg, @class]} {@rest}>
|
||||
<button
|
||||
:if={@dismissable_id}
|
||||
class="absolute right-0 top-0 m-2 text-yellow-800 dark:text-yellow-900"
|
||||
class={"absolute right-0 top-0 m-2 #{@theme.title_text}"}
|
||||
onclick={"localStorage['notice_dismissed__#{@dismissable_id}'] = 'true'; document.getElementById('#{@dismissable_id}').classList.add('hidden')"}
|
||||
>
|
||||
<Heroicons.x_mark class="h-4 w-4 hover:stroke-2" />
|
||||
</button>
|
||||
<div class="flex">
|
||||
<div :if={@size !== :xs} class="flex-shrink-0">
|
||||
<div :if={@title} class="flex-shrink-0">
|
||||
<svg
|
||||
class="h-5 w-5 text-yellow-400"
|
||||
class={"h-5 w-5 #{@theme.icon}"}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
@ -91,14 +109,11 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3
|
||||
:if={@size !== :xs}
|
||||
class={"text-#{@size} font-medium text-yellow-800 dark:text-yellow-900 mb-2"}
|
||||
>
|
||||
<div class={["w-full", @title && "ml-3"]}>
|
||||
<h3 :if={@title} class={"text-#{@size} font-medium #{@theme.title_text} mb-2"}>
|
||||
<%= @title %>
|
||||
</h3>
|
||||
<div class={"text-#{@size} text-yellow-700 dark:text-yellow-800"}>
|
||||
<div class={"text-#{@size} #{@theme.body_text}"}>
|
||||
<p>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</p>
|
||||
@ -107,7 +122,7 @@ defmodule PlausibleWeb.Components.Generic do
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-key={@dismissable_id}>
|
||||
<script :if={@dismissable_id} data-key={@dismissable_id}>
|
||||
const dismissId = document.currentScript.dataset.key
|
||||
const localStorageKey = `notice_dismissed__${dismissId}`
|
||||
|
||||
|
@ -25,41 +25,10 @@
|
||||
|
||||
<div class="my-4 border-b border-gray-400"></div>
|
||||
|
||||
<div
|
||||
:if={
|
||||
@subscription && @subscription.status == Plausible.Billing.Subscription.Status.deleted()
|
||||
}
|
||||
class="p-2 bg-red-100 rounded-lg sm:p-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between">
|
||||
<div class="flex items-center flex-1 w-0">
|
||||
<svg
|
||||
class="w-6 h-6 text-red-800"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 9V11M12 15H12.01M5.07183 19H18.9282C20.4678 19 21.4301 17.3333 20.6603 16L13.7321 4C12.9623 2.66667 11.0378 2.66667 10.268 4L3.33978 16C2.56998 17.3333 3.53223 19 5.07183 19Z"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<p class="ml-3 font-medium text-red-800">
|
||||
<%= if @subscription.next_bill_date && Timex.compare(@subscription.next_bill_date, Timex.today()) >= 0 do %>
|
||||
Your subscription is cancelled but you have access to your stats until <%= Timex.format!(
|
||||
@subscription.next_bill_date,
|
||||
"{Mshort} {D}, {YYYY}"
|
||||
) %>. Upgrade below to make sure you don't lose access.
|
||||
<% else %>
|
||||
Your subscription is cancelled. Upgrade below to get access to your stats again.
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PlausibleWeb.Components.Billing.subscription_cancelled_notice
|
||||
user={@user}
|
||||
dismissable={false}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col items-center justify-between mt-8 sm:flex-row sm:items-start">
|
||||
<PlausibleWeb.Components.Billing.monthly_quota_box
|
||||
|
@ -96,15 +96,19 @@
|
||||
<% end %>
|
||||
|
||||
<%= if @conn.assigns[:current_user] do %>
|
||||
<.subscription_past_due_notice
|
||||
subscription={@conn.assigns.current_user.subscription}
|
||||
class="container"
|
||||
/>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<.subscription_cancelled_notice user={@conn.assigns.current_user} />
|
||||
|
||||
<.subscription_paused_notice
|
||||
subscription={@conn.assigns.current_user.subscription}
|
||||
class="container"
|
||||
/>
|
||||
<.subscription_past_due_notice
|
||||
subscription={@conn.assigns.current_user.subscription}
|
||||
class="container"
|
||||
/>
|
||||
|
||||
<.private_preview_end_notice user={@conn.assigns.current_user} />
|
||||
<.subscription_paused_notice
|
||||
subscription={@conn.assigns.current_user.subscription}
|
||||
class="container"
|
||||
/>
|
||||
|
||||
<.private_preview_end_notice user={@conn.assigns.current_user} />
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -5,56 +5,92 @@ defmodule Plausible.Billing.PlansTest do
|
||||
@legacy_plan_id "558746"
|
||||
@v1_plan_id "558018"
|
||||
@v2_plan_id "654177"
|
||||
@v3_plan_id "749342"
|
||||
@v4_plan_id "857097"
|
||||
@v3_business_plan_id "857481"
|
||||
@v4_business_plan_id "857105"
|
||||
|
||||
describe "getting subscription plans for user" do
|
||||
test "growth_plans_for/1 returns v1 plans for a user on a legacy plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
|
||||
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v1_plan_id
|
||||
insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
|
||||
|> Plans.growth_plans_for()
|
||||
|> assert_generation(1)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 returns v1 plans for users who are already on v1 pricing" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v1_plan_id
|
||||
insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
|> Plans.growth_plans_for()
|
||||
|> assert_generation(1)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 returns v2 plans for users who are already on v2 pricing" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
|
||||
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v2_plan_id
|
||||
insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
|
||||
|> Plans.growth_plans_for()
|
||||
|> assert_generation(2)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 shows v3 pricing for users who signed up before the business tier" do
|
||||
user = insert(:user, inserted_at: ~U[2023-10-01T00:00:00Z])
|
||||
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v3_plan_id
|
||||
test "growth_plans_for/1 returns v4 plans for invited users with trial_expiry = nil" do
|
||||
insert(:user, trial_expiry_date: nil)
|
||||
|> Plans.growth_plans_for()
|
||||
|> assert_generation(4)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 returns v4 plans for users whose trial started after the business tiers release" do
|
||||
insert(:user, trial_expiry_date: ~D[2023-12-24])
|
||||
|> Plans.growth_plans_for()
|
||||
|> assert_generation(4)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 returns v3 plans for pre business tier trials only if their trial is active or expired less than 10 days ago" do
|
||||
trial_start = ~D[2023-10-27]
|
||||
trial_expiry = Timex.shift(trial_start, days: 30)
|
||||
expiry_datetime = Timex.to_datetime(trial_expiry)
|
||||
|
||||
user = insert(:user, trial_expiry_date: trial_expiry)
|
||||
|
||||
now1 = Timex.shift(expiry_datetime, days: -1)
|
||||
now2 = Timex.shift(expiry_datetime, days: 10)
|
||||
now3 = Timex.shift(expiry_datetime, days: 11)
|
||||
|
||||
Plans.growth_plans_for(user, now1) |> assert_generation(3)
|
||||
Plans.growth_plans_for(user, now2) |> assert_generation(3)
|
||||
Plans.growth_plans_for(user, now3) |> assert_generation(4)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 returns v4 plans for expired legacy subscriptions" do
|
||||
subscription =
|
||||
build(:subscription,
|
||||
paddle_plan_id: @v1_plan_id,
|
||||
status: :deleted,
|
||||
next_bill_date: ~D[2023-11-10]
|
||||
)
|
||||
|
||||
insert(:user, subscription: subscription)
|
||||
|> Plans.growth_plans_for()
|
||||
|> assert_generation(4)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 shows v4 plans for everyone else" do
|
||||
user = insert(:user, inserted_at: ~U[2024-01-01T00:00:00Z])
|
||||
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v4_plan_id
|
||||
insert(:user, inserted_at: ~U[2024-01-01T00:00:00Z])
|
||||
|> Plans.growth_plans_for()
|
||||
|> assert_generation(4)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 does not return business plans" do
|
||||
user = insert(:user)
|
||||
|
||||
Plans.growth_plans_for(user)
|
||||
insert(:user)
|
||||
|> Plans.growth_plans_for()
|
||||
|> Enum.each(fn plan ->
|
||||
assert plan.kind != :business
|
||||
end)
|
||||
end
|
||||
|
||||
test "growth_plans_for/1 returns the latest generation of growth plans for a user with a business subscription" do
|
||||
user =
|
||||
insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id))
|
||||
|
||||
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v4_plan_id
|
||||
insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id))
|
||||
|> Plans.growth_plans_for()
|
||||
|> assert_generation(4)
|
||||
end
|
||||
|
||||
test "business_plans_for/1 returns v3 business plans for a user on a legacy plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
|
||||
assert List.first(Plans.business_plans_for(user)).monthly_product_id == @v3_business_plan_id
|
||||
insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
|
||||
|> Plans.business_plans_for()
|
||||
|> assert_generation(3)
|
||||
end
|
||||
|
||||
test "business_plans_for/1 returns v3 business plans for a v2 subscriber" do
|
||||
@ -63,12 +99,48 @@ defmodule Plausible.Billing.PlansTest do
|
||||
business_plans = Plans.business_plans_for(user)
|
||||
|
||||
assert Enum.all?(business_plans, &(&1.kind == :business))
|
||||
assert List.first(business_plans).monthly_product_id == @v3_business_plan_id
|
||||
assert_generation(business_plans, 3)
|
||||
end
|
||||
|
||||
test "business_plans_for/1 returns v3 business plans for users who signed up before the business tier release" do
|
||||
user = insert(:user, inserted_at: ~U[2023-10-01T00:00:00Z])
|
||||
assert List.first(Plans.business_plans_for(user)).monthly_product_id == @v3_business_plan_id
|
||||
test "business_plans_for/1 returns v4 plans for invited users with trial_expiry = nil" do
|
||||
insert(:user, trial_expiry_date: nil)
|
||||
|> Plans.business_plans_for()
|
||||
|> assert_generation(4)
|
||||
end
|
||||
|
||||
test "business_plans_for/1 returns v4 plans for users whose trial started after the business tiers release" do
|
||||
insert(:user, trial_expiry_date: ~D[2023-12-24])
|
||||
|> Plans.business_plans_for()
|
||||
|> assert_generation(4)
|
||||
end
|
||||
|
||||
test "business_plans_for/1 returns v3 plans for pre business tier trials only if their trial is active or expired less than 10 days ago" do
|
||||
trial_start = ~D[2023-10-27]
|
||||
trial_expiry = Timex.shift(trial_start, days: 30)
|
||||
expiry_datetime = Timex.to_datetime(trial_expiry)
|
||||
|
||||
user = insert(:user, trial_expiry_date: trial_expiry)
|
||||
|
||||
now1 = Timex.shift(expiry_datetime, days: -1)
|
||||
now2 = Timex.shift(expiry_datetime, days: 10)
|
||||
now3 = Timex.shift(expiry_datetime, days: 11)
|
||||
|
||||
Plans.business_plans_for(user, now1) |> assert_generation(3)
|
||||
Plans.business_plans_for(user, now2) |> assert_generation(3)
|
||||
Plans.business_plans_for(user, now3) |> assert_generation(4)
|
||||
end
|
||||
|
||||
test "business_plans_for/1 returns v4 plans for expired legacy subscriptions" do
|
||||
subscription =
|
||||
build(:subscription,
|
||||
paddle_plan_id: @v2_plan_id,
|
||||
status: :deleted,
|
||||
next_bill_date: ~D[2023-11-10]
|
||||
)
|
||||
|
||||
insert(:user, subscription: subscription)
|
||||
|> Plans.business_plans_for()
|
||||
|> assert_generation(4)
|
||||
end
|
||||
|
||||
test "business_plans_for/1 returns v4 business plans for everyone else" do
|
||||
@ -76,7 +148,7 @@ defmodule Plausible.Billing.PlansTest do
|
||||
business_plans = Plans.business_plans_for(user)
|
||||
|
||||
assert Enum.all?(business_plans, &(&1.kind == :business))
|
||||
assert List.first(business_plans).monthly_product_id == @v4_business_plan_id
|
||||
assert_generation(business_plans, 4)
|
||||
end
|
||||
|
||||
test "available_plans returns all plans for user with prices when asked for" do
|
||||
@ -275,4 +347,8 @@ defmodule Plausible.Billing.PlansTest do
|
||||
assert Plans.suggest_tier(user) == :business
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_generation(plans_list, generation) do
|
||||
assert List.first(plans_list).generation == generation
|
||||
end
|
||||
end
|
||||
|
@ -16,6 +16,7 @@ defmodule PlausibleWeb.AuthControllerTest do
|
||||
setup :verify_on_exit!
|
||||
|
||||
@v3_plan_id "749355"
|
||||
@v4_plan_id "857097"
|
||||
@configured_enterprise_plan_paddle_plan_id "123"
|
||||
|
||||
describe "GET /register" do
|
||||
@ -627,6 +628,67 @@ defmodule PlausibleWeb.AuthControllerTest do
|
||||
Routes.billing_path(conn, :choose_plan)
|
||||
end
|
||||
|
||||
test "renders cancelled subscription notice", %{conn: conn, user: user} do
|
||||
insert(:subscription,
|
||||
paddle_plan_id: @v4_plan_id,
|
||||
user: user,
|
||||
status: :deleted,
|
||||
next_bill_date: ~D[2023-01-01]
|
||||
)
|
||||
|
||||
notice_text =
|
||||
get(conn, "/settings")
|
||||
|> html_response(200)
|
||||
|> text_of_element("#global-subscription-cancelled-notice")
|
||||
|
||||
assert notice_text =~ "Subscription cancelled"
|
||||
assert notice_text =~ "Upgrade your subscription to get access to your stats again"
|
||||
end
|
||||
|
||||
test "renders cancelled subscription notice with some subscription days still left", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
insert(:subscription,
|
||||
paddle_plan_id: @v4_plan_id,
|
||||
user: user,
|
||||
status: :deleted,
|
||||
next_bill_date: Timex.shift(Timex.today(), days: 10)
|
||||
)
|
||||
|
||||
notice_text =
|
||||
get(conn, "/settings")
|
||||
|> html_response(200)
|
||||
|> text_of_element("#global-subscription-cancelled-notice")
|
||||
|
||||
assert notice_text =~ "Subscription cancelled"
|
||||
assert notice_text =~ "You have access to your stats until"
|
||||
assert notice_text =~ "Upgrade your subscription to make sure you don't lose access"
|
||||
end
|
||||
|
||||
test "renders cancelled subscription notice with a warning about losing grandfathering", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
insert(:subscription,
|
||||
paddle_plan_id: @v3_plan_id,
|
||||
user: user,
|
||||
status: :deleted,
|
||||
next_bill_date: Timex.shift(Timex.today(), days: 10)
|
||||
)
|
||||
|
||||
notice_text =
|
||||
get(conn, "/settings")
|
||||
|> html_response(200)
|
||||
|> text_of_element("#global-subscription-cancelled-notice")
|
||||
|
||||
assert notice_text =~ "Subscription cancelled"
|
||||
assert notice_text =~ "You have access to your stats until"
|
||||
|
||||
assert notice_text =~
|
||||
"by letting your subscription expire, you lose access to our grandfathered terms"
|
||||
end
|
||||
|
||||
test "shows invoices for subscribed user", %{conn: conn, user: user} do
|
||||
insert(:subscription,
|
||||
paddle_plan_id: "558018",
|
||||
|
@ -34,7 +34,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
|
||||
|
||||
describe "for a legacy trial (user registered before business tiers release)" do
|
||||
setup %{conn: conn} do
|
||||
user = insert(:user, inserted_at: ~N[2023-10-25 12:00:00])
|
||||
user = insert(:user, trial_expiry_date: ~D[2023-11-24])
|
||||
{:ok, conn: conn} = log_in(%{conn: conn, user: user})
|
||||
{:ok, conn: conn, user: user}
|
||||
end
|
||||
|
@ -35,7 +35,12 @@ defmodule Plausible.TestUtils do
|
||||
end
|
||||
|
||||
def create_user(_) do
|
||||
{:ok, user: Factory.insert(:user, inserted_at: ~U[2024-01-01T00:00:00Z])}
|
||||
{:ok,
|
||||
user:
|
||||
Factory.insert(:user,
|
||||
inserted_at: ~U[2024-01-01T00:00:00Z],
|
||||
trial_expiry_date: ~D[2024-02-01]
|
||||
)}
|
||||
end
|
||||
|
||||
def create_site(%{user: user}) do
|
||||
|
Loading…
Reference in New Issue
Block a user