Starter tier: Upgrade page remodelling (#5394)

* add a new (feature flagged) upgrade page offering v5 plans

* include starter tier plans in available_plans_for + use dev prices in test

* upgrade page remodelling with starter tier

* mobile optimizations

* optimize for darkmode

* add embedded dashboards as a growth benefit

* do not hide header on LegacyChoosePlan

* consistent v5 plan feature order

* slight grandfathering notice adjustment

* display monthly price too on yearly plans

* default to v5 plans unlesss legacy? is true

* refactor: suggest volume not plan for emails

* align back link with page title

* render grandfathering notice for growth v4 too
This commit is contained in:
RobertJoonas 2025-05-20 14:22:12 +01:00 committed by GitHub
parent 97449613e1
commit 2dd144bf85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 3028 additions and 472 deletions

View File

@ -17,7 +17,7 @@ config :plausible, Plausible.ClickhouseRepo,
config :plausible, Plausible.Mailer, adapter: Bamboo.TestAdapter
config :plausible,
paddle_api: Plausible.PaddleApi.Mock,
paddle_api: Plausible.Billing.TestPaddleApiMock,
google_api: Plausible.Google.API.Mock
config :bamboo, :refute_timeout, 10

View File

@ -16,7 +16,7 @@ defmodule Plausible.Billing.Plan do
# production plans, contain multiple generations of plans in the same file
# for testing purposes.
field :generation, :integer
field :kind, Ecto.Enum, values: [:growth, :business]
field :kind, Ecto.Enum, values: [:starter, :growth, :business]
field :features, Plausible.Billing.Ecto.FeatureList
field :monthly_pageview_limit, :integer

View File

@ -4,7 +4,7 @@ defmodule Plausible.Billing.Plans do
alias Plausible.Billing.{Subscription, Plan, EnterprisePlan}
alias Plausible.Teams
@generations [:legacy_plans, :plans_v1, :plans_v2, :plans_v3, :plans_v4]
@generations [:legacy_plans, :plans_v1, :plans_v2, :plans_v3, :plans_v4, :plans_v5]
for group <- Enum.flat_map(@generations, &[&1, :"sandbox_#{&1}"]) do
path = Application.app_dir(:plausible, ["priv", "#{group}.json"])
@ -32,41 +32,62 @@ defmodule Plausible.Billing.Plans do
end
end
@spec growth_plans_for(Subscription.t()) :: [Plan.t()]
defp starter_plans_for(legacy?) do
if legacy? do
[]
else
Enum.filter(plans_v5(), &(&1.kind == :starter))
end
end
@spec growth_plans_for(Subscription.t(), boolean()) :: [Plan.t()]
@doc """
Returns a list of growth plans available for the subscription to choose.
As new versions of plans are introduced, subscriptions which were on old plans can
still choose from old plans.
"""
def growth_plans_for(subscription) do
def growth_plans_for(subscription, legacy? \\ false) do
owned_plan = get_regular_plan(subscription)
default_plans = if legacy?, do: plans_v4(), else: plans_v5()
cond do
is_nil(owned_plan) -> plans_v4()
subscription && Subscriptions.expired?(subscription) -> plans_v4()
owned_plan.kind == :business -> plans_v4()
is_nil(owned_plan) -> default_plans
subscription && Subscriptions.expired?(subscription) -> default_plans
owned_plan.kind == :business -> default_plans
owned_plan.generation == 1 -> plans_v1() |> drop_high_plans(owned_plan)
owned_plan.generation == 2 -> plans_v2() |> drop_high_plans(owned_plan)
owned_plan.generation == 3 -> plans_v3()
owned_plan.generation == 4 -> plans_v4()
owned_plan.generation == 5 -> plans_v5()
end
|> Enum.filter(&(&1.kind == :growth))
end
def business_plans_for(subscription) do
def business_plans_for(subscription, legacy? \\ false) do
owned_plan = get_regular_plan(subscription)
default_plans = if legacy?, do: plans_v4(), else: plans_v5()
cond do
subscription && Subscriptions.expired?(subscription) -> plans_v4()
subscription && Subscriptions.expired?(subscription) -> default_plans
owned_plan && owned_plan.generation < 4 -> plans_v3()
true -> plans_v4()
owned_plan && owned_plan.generation < 5 -> plans_v4()
true -> default_plans
end
|> Enum.filter(&(&1.kind == :business))
end
def available_plans_for(subscription, opts \\ []) do
plans = growth_plans_for(subscription) ++ business_plans_for(subscription)
legacy? = Keyword.get(opts, :legacy?, false)
plans =
Enum.concat([
starter_plans_for(legacy?),
growth_plans_for(subscription, legacy?),
business_plans_for(subscription, legacy?)
])
plans =
if Keyword.get(opts, :with_prices) do
@ -192,42 +213,31 @@ defmodule Plausible.Billing.Plans do
end
@doc """
Returns the most appropriate plan for a team based on its usage during a
given cycle.
Returns the most appropriate monthly pageview volume for a given usage cycle.
The cycle is either last 30 days (for trials) or last billing cycle for teams
with an existing subscription.
The generation and tier from which we're searching for a suitable volume doesn't
matter - the monthly pageview volumes for all plans starting from v3 are going from
10k to 10M. This function uses v4 Growth but it might as well be e.g. v5 Business.
If the usage during the cycle exceeds the enterprise-level threshold, or if
the team already has an enterprise plan, it suggests the :enterprise
plan.
Otherwise, it recommends the plan where the cycle usage falls just under the
plan's limit from the available options for the team.
the team already has an enterprise plan, it returns `:enterprise`. Otherwise,
a string representing the volume, e.g. "100k" or "5M".
"""
@enterprise_level_usage 10_000_000
@spec suggest(Teams.Team.t(), non_neg_integer()) :: Plan.t()
def suggest(team, usage_during_cycle) do
cond do
usage_during_cycle > @enterprise_level_usage ->
:enterprise
Teams.Billing.enterprise_configured?(team) ->
:enterprise
true ->
subscription = Teams.Billing.get_subscription(team)
suggest_by_usage(subscription, usage_during_cycle)
@spec suggest_volume(Teams.Team.t(), non_neg_integer()) :: String.t() | :enterprise
def suggest_volume(team, usage_during_cycle) do
if Teams.Billing.enterprise_configured?(team) do
:enterprise
else
plans_v4()
|> Enum.filter(&(&1.kind == :growth))
|> Enum.find(%{volume: :enterprise}, &(usage_during_cycle < &1.monthly_pageview_limit))
|> Map.get(:volume)
end
end
def suggest_by_usage(subscription, usage_during_cycle) do
available_plans =
if business_tier?(subscription),
do: business_plans_for(subscription),
else: growth_plans_for(subscription)
Enum.find(available_plans, &(usage_during_cycle < &1.monthly_pageview_limit))
end
def all() do
legacy_plans() ++ plans_v1() ++ plans_v2() ++ plans_v3() ++ plans_v4()
legacy_plans() ++ plans_v1() ++ plans_v2() ++ plans_v3() ++ plans_v4() ++ plans_v5()
end
end

View File

@ -57,12 +57,35 @@ defmodule Plausible.Billing.Quota do
`:custom` is returned. This means that this kind of usage should get on
a custom plan.
To avoid confusion, we do not recommend Growth tiers for customers that
are already on a Business tier (even if their usage would fit Growth).
To avoid confusion, we do not recommend a lower tier for customers that
are already on a higher tier (even if their usage is low enough).
`nil` is returned if the usage is not eligible for upgrade.
"""
def suggest_tier(usage, highest_growth, highest_business, owned_tier) do
def suggest_tier(usage, highest_starter, highest_growth, highest_business, owned_tier) do
cond do
not eligible_for_upgrade?(usage) ->
nil
usage_fits_plan?(usage, highest_starter) and owned_tier not in [:business, :growth] ->
:starter
usage_fits_plan?(usage, highest_growth) and owned_tier != :business ->
:growth
usage_fits_plan?(usage, highest_business) ->
:business
true ->
:custom
end
end
@doc """
[DEPRECATED] Used in LegacyChoosePlan in order to suggest a tier
when `starter_tier` flag is not enabled.
"""
def legacy_suggest_tier(usage, highest_growth, highest_business, owned_tier) do
cond do
not eligible_for_upgrade?(usage) -> nil
usage_fits_plan?(usage, highest_growth) and owned_tier != :business -> :growth

View File

@ -55,11 +55,11 @@ defmodule Plausible.Billing.SiteLocker do
defp send_grace_period_end_email(team, true) do
team = Repo.preload(team, [:owners, :billing_members])
usage = Teams.Billing.monthly_pageview_usage(team)
suggested_plan = Plausible.Billing.Plans.suggest(team, usage.last_cycle.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(team, usage.last_cycle.total)
for recipient <- team.owners ++ team.billing_members do
recipient
|> PlausibleWeb.Email.dashboard_locked(team, usage, suggested_plan)
|> PlausibleWeb.Email.dashboard_locked(team, usage, suggested_volume)
|> Plausible.Mailer.send()
end
end

View File

@ -0,0 +1,141 @@
defmodule PlausibleWeb.Components.Billing.LegacyPlanBenefits do
@moduledoc """
[DEPRECATED] This file is essentially a copy of
`PlausibleWeb.Components.Billing.PlanBenefits` with the
intent of keeping the old behaviour in place for the users without
the `starter_tier` feature flag enabled.
"""
use Phoenix.Component
alias Plausible.Billing.Plan
attr :benefits, :list, required: true
attr :class, :string, default: nil
@doc """
This function takes a list of benefits returned by either one of:
* `for_growth/1`
* `for_business/2`
* `for_enterprise/1`.
and renders them as HTML.
The benefits in the given list can be either strings or functions
returning a Phoenix component. This allows, for example, to render
links within the plan benefit text.
"""
def render(assigns) do
~H"""
<ul role="list" class={["mt-8 space-y-3 text-sm leading-6 xl:mt-10", @class]}>
<li :for={benefit <- @benefits} class="flex gap-x-3">
<Heroicons.check class="h-6 w-5 text-indigo-600 dark:text-green-600" />
{if is_binary(benefit), do: benefit, else: benefit.(assigns)}
</li>
</ul>
"""
end
@doc """
This function takes a growth plan and returns a list representing
the different benefits a user gets when subscribing to this plan.
"""
def for_growth(plan) do
[
team_member_limit_benefit(plan),
site_limit_benefit(plan),
data_retention_benefit(plan),
"Intuitive, fast and privacy-friendly dashboard",
"Email/Slack reports",
"Google Analytics import"
]
|> Kernel.++(feature_benefits(plan))
|> Kernel.++(["Saved Segments"])
|> Enum.filter(& &1)
end
@doc """
Returns Business benefits for the given Business plan.
A second argument is also required - list of Growth benefits. This
is because we don't want to list the same benefits in both Growth
and Business. Everything in Growth is also included in Business.
"""
def for_business(plan, growth_benefits) do
[
"Everything in Growth",
team_member_limit_benefit(plan),
site_limit_benefit(plan),
data_retention_benefit(plan)
]
|> Kernel.++(feature_benefits(plan))
|> Kernel.--(growth_benefits)
|> Kernel.++(["Priority support"])
|> Enum.filter(& &1)
end
@doc """
This function only takes a list of business benefits. Since all
limits and features of enterprise plans are configurable, we can
say on the upgrade page that enterprise plans include everything
in Business.
"""
def for_enterprise(business_benefits) do
team_members =
if "Up to 10 team members" in business_benefits, do: "10+ team members"
data_retention =
if "5 years of data retention" in business_benefits, do: "5+ years of data retention"
[
"Everything in Business",
team_members,
"50+ sites",
"600+ Stats API requests per hour",
&sites_api_benefit/1,
data_retention,
"Technical onboarding"
]
|> Enum.filter(& &1)
end
defp data_retention_benefit(%Plan{} = plan) do
if plan.data_retention_in_years, do: "#{plan.data_retention_in_years} years of data retention"
end
defp team_member_limit_benefit(%Plan{} = plan) do
case plan.team_member_limit do
:unlimited -> "Unlimited team members"
number -> "Up to #{number} team members"
end
end
defp site_limit_benefit(%Plan{} = plan), do: "Up to #{plan.site_limit} sites"
defp feature_benefits(%Plan{} = plan) do
Enum.flat_map(plan.features, fn feature_mod ->
case feature_mod.name() do
:goals -> ["Goals and custom events"]
:teams -> []
:shared_links -> []
:stats_api -> ["Stats API (600 requests per hour)", "Looker Studio Connector"]
:revenue_goals -> ["Ecommerce revenue attribution"]
_ -> [feature_mod.display_name()]
end
end)
end
defp sites_api_benefit(assigns) do
~H"""
<p>
Sites API access for
<.link
class="text-indigo-500 hover:text-indigo-400"
href="https://plausible.io/white-label-web-analytics"
>
reselling
</.link>
</p>
"""
end
end

View File

@ -0,0 +1,378 @@
defmodule PlausibleWeb.Components.Billing.LegacyPlanBox do
@moduledoc """
[DEPRECATED] This file is essentially a copy of
`PlausibleWeb.Components.Billing.PlanBox` with the
intent of keeping the old behaviour in place for the users without
the `starter_tier` feature flag enabled.
"""
use PlausibleWeb, :component
require Plausible.Billing.Subscription.Status
alias PlausibleWeb.Components.Billing.{PlanBenefits, Notice}
alias Plausible.Billing.{Plan, Quota, Subscription}
def standard(assigns) do
highlight =
cond do
assigns.owned && assigns.recommended -> "Current"
assigns.recommended -> "Recommended"
true -> nil
end
assigns = assign(assigns, :highlight, highlight)
~H"""
<div
id={"#{@kind}-plan-box"}
class={[
"shadow-lg bg-white rounded-3xl px-6 sm:px-8 py-4 sm:py-6 dark:bg-gray-800",
!@highlight && "dark:ring-gray-600",
@highlight && "ring-2 ring-indigo-600 dark:ring-indigo-300"
]}
>
<div class="flex items-center justify-between gap-x-4">
<h3 class={[
"text-lg font-semibold leading-8",
!@highlight && "text-gray-900 dark:text-gray-100",
@highlight && "text-indigo-600 dark:text-indigo-300"
]}>
{String.capitalize(to_string(@kind))}
</h3>
<.pill :if={@highlight} text={@highlight} />
</div>
<div>
<.render_price_info available={@available} {assigns} />
<%= if @available do %>
<.checkout id={"#{@kind}-checkout"} {assigns} />
<% else %>
<.contact_button class="bg-indigo-600 hover:bg-indigo-500 text-white" />
<% end %>
</div>
<%= if @owned && @kind == :growth && @plan_to_render.generation < 4 do %>
<Notice.growth_grandfathered />
<% else %>
<PlanBenefits.render benefits={@benefits} class="text-gray-600 dark:text-gray-100" />
<% end %>
</div>
"""
end
def enterprise(assigns) do
~H"""
<div
id="enterprise-plan-box"
class={[
"rounded-3xl px-6 sm:px-8 py-4 sm:py-6 bg-gray-900 shadow-xl dark:bg-gray-800",
!@recommended && "dark:ring-gray-600",
@recommended && "ring-4 ring-indigo-500 dark:ring-2 dark:ring-indigo-300"
]}
>
<div class="flex items-center justify-between gap-x-4">
<h3 class={[
"text-lg font-semibold leading-8",
!@recommended && "text-white dark:text-gray-100",
@recommended && "text-indigo-400 dark:text-indigo-300"
]}>
Enterprise
</h3>
<span
:if={@recommended}
id="enterprise-highlight-pill"
class="rounded-full ring-1 ring-indigo-500 px-2.5 py-1 text-xs font-semibold leading-5 text-indigo-400 dark:text-indigo-300 dark:ring-1 dark:ring-indigo-300/50"
>
Recommended
</span>
</div>
<p class="mt-6 flex items-baseline gap-x-1">
<span class="text-4xl font-bold tracking-tight text-white dark:text-gray-100">
Custom
</span>
</p>
<p class="h-4 mt-1"></p>
<.contact_button class="" />
<PlanBenefits.render benefits={@benefits} class="text-gray-300 dark:text-gray-100" />
</div>
"""
end
defp pill(assigns) do
~H"""
<div class="flex items-center justify-between gap-x-4">
<p
id="highlight-pill"
class="rounded-full bg-indigo-600/10 px-2.5 py-1 text-xs font-semibold leading-5 text-indigo-600 dark:text-indigo-300 dark:ring-1 dark:ring-indigo-300/50"
>
{@text}
</p>
</div>
"""
end
defp render_price_info(%{available: false} = assigns) do
~H"""
<p id={"#{@kind}-custom-price"} class="mt-6 flex items-baseline gap-x-1">
<span class="text-4xl font-bold tracking-tight text-gray-900 dark:text-white">
Custom
</span>
</p>
<p class="h-4 mt-1"></p>
"""
end
defp render_price_info(assigns) do
~H"""
<p class="mt-6 flex items-baseline gap-x-1">
<.price_tag
kind={@kind}
selected_interval={@selected_interval}
plan_to_render={@plan_to_render}
/>
</p>
<p class="mt-1 text-xs">+ VAT if applicable</p>
"""
end
defp price_tag(%{plan_to_render: %Plan{monthly_cost: nil}} = assigns) do
~H"""
<span class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100">
N/A
</span>
"""
end
defp price_tag(%{selected_interval: :monthly} = assigns) do
~H"""
<span
id={"#{@kind}-price-tag-amount"}
class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100"
>
{@plan_to_render.monthly_cost |> Plausible.Billing.format_price()}
</span>
<span
id={"#{@kind}-price-tag-interval"}
class="text-sm font-semibold leading-6 text-gray-600 dark:text-gray-500"
>
/month
</span>
"""
end
defp price_tag(%{selected_interval: :yearly} = assigns) do
~H"""
<span class="text-2xl font-bold w-max tracking-tight line-through text-gray-500 dark:text-gray-600 mr-1">
{@plan_to_render.monthly_cost |> Money.mult!(12) |> Plausible.Billing.format_price()}
</span>
<span
id={"#{@kind}-price-tag-amount"}
class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100"
>
{@plan_to_render.yearly_cost |> Plausible.Billing.format_price()}
</span>
<span id={"#{@kind}-price-tag-interval"} class="text-sm font-semibold leading-6 text-gray-600">
/year
</span>
"""
end
defp checkout(assigns) do
paddle_product_id = get_paddle_product_id(assigns.plan_to_render, assigns.selected_interval)
change_plan_link_text = change_plan_link_text(assigns)
subscription =
Plausible.Teams.Billing.get_subscription(assigns.current_team)
billing_details_expired =
Subscription.Status.in?(subscription, [
Subscription.Status.paused(),
Subscription.Status.past_due()
])
subscription_deleted = Subscription.Status.deleted?(subscription)
usage_check = check_usage_within_plan_limits(assigns)
{checkout_disabled, disabled_message} =
cond do
not Quota.eligible_for_upgrade?(assigns.usage) ->
{true, nil}
change_plan_link_text == "Currently on this plan" && not subscription_deleted ->
{true, nil}
usage_check != :ok ->
{true, "Your usage exceeds this plan"}
billing_details_expired ->
{true, "Please update your billing details first"}
true ->
{false, nil}
end
exceeded_plan_limits =
case usage_check do
{:error, {:over_plan_limits, limits}} ->
limits
_ ->
[]
end
feature_usage_check = Quota.ensure_feature_access(assigns.usage, assigns.plan_to_render)
assigns =
assigns
|> assign(:paddle_product_id, paddle_product_id)
|> assign(:change_plan_link_text, change_plan_link_text)
|> assign(:checkout_disabled, checkout_disabled)
|> assign(:disabled_message, disabled_message)
|> assign(:exceeded_plan_limits, exceeded_plan_limits)
|> assign(:confirm_message, losing_features_message(feature_usage_check))
~H"""
<%= if @owned_plan && Plausible.Billing.Subscriptions.resumable?(@current_team.subscription) do %>
<.change_plan_link {assigns} />
<% else %>
<PlausibleWeb.Components.Billing.paddle_button
user={@current_user}
team={@current_team}
{assigns}
>
Upgrade
</PlausibleWeb.Components.Billing.paddle_button>
<% end %>
<.tooltip :if={@exceeded_plan_limits != [] && @disabled_message}>
<div class="pt-2 text-sm w-full flex items-center text-red-700 dark:text-red-500 justify-center">
{@disabled_message}
<Heroicons.information_circle class="hidden sm:block w-5 h-5 sm:ml-2" />
</div>
<:tooltip_content>
Your usage exceeds the following limit(s):<br /><br />
<p :for={limit <- @exceeded_plan_limits}>
{Phoenix.Naming.humanize(limit)}<br />
</p>
</:tooltip_content>
</.tooltip>
<div
:if={@disabled_message && @exceeded_plan_limits == []}
class="pt-2 text-sm w-full text-red-700 dark:text-red-500 text-center"
>
{@disabled_message}
</div>
"""
end
defp check_usage_within_plan_limits(%{available: false}) do
{:error, :plan_unavailable}
end
defp check_usage_within_plan_limits(%{
available: true,
usage: usage,
current_team: current_team,
plan_to_render: plan
}) do
# At this point, the user is *not guaranteed* to have a team,
# with ongoing trial.
trial_active_or_ended_recently? =
not is_nil(current_team) and not is_nil(current_team.trial_expiry_date) and
Plausible.Teams.trial_days_left(current_team) >= -10
limit_checking_opts =
cond do
current_team && current_team.allow_next_upgrade_override ->
[ignore_pageview_limit: true]
trial_active_or_ended_recently? && plan.volume == "10k" ->
[pageview_allowance_margin: 0.3]
trial_active_or_ended_recently? ->
[pageview_allowance_margin: 0.15]
true ->
[]
end
Quota.ensure_within_plan_limits(usage, plan, limit_checking_opts)
end
defp get_paddle_product_id(%Plan{monthly_product_id: plan_id}, :monthly), do: plan_id
defp get_paddle_product_id(%Plan{yearly_product_id: plan_id}, :yearly), do: plan_id
defp change_plan_link_text(
%{
owned_plan: %Plan{kind: from_kind, monthly_pageview_limit: from_volume},
plan_to_render: %Plan{kind: to_kind, monthly_pageview_limit: to_volume},
current_interval: from_interval,
selected_interval: to_interval
} = _assigns
) do
cond do
from_kind == :business && to_kind == :growth ->
"Downgrade to Growth"
from_kind == :growth && to_kind == :business ->
"Upgrade to Business"
from_volume == to_volume && from_interval == to_interval ->
"Currently on this plan"
from_volume == to_volume ->
"Change billing interval"
from_volume > to_volume ->
"Downgrade"
true ->
"Upgrade"
end
end
defp change_plan_link_text(_), do: nil
defp change_plan_link(assigns) do
confirmed =
if assigns.confirm_message, do: "confirm(\"#{assigns.confirm_message}\")", else: "true"
assigns = assign(assigns, :confirmed, confirmed)
~H"""
<button
id={"#{@kind}-checkout"}
onclick={"if (#{@confirmed}) {window.location = '#{Routes.billing_path(PlausibleWeb.Endpoint, :change_plan_preview, @paddle_product_id)}'}"}
class={[
"w-full mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 text-white",
!@checkout_disabled && "bg-indigo-600 hover:bg-indigo-500",
@checkout_disabled && "pointer-events-none bg-gray-400 dark:bg-gray-600"
]}
>
{@change_plan_link_text}
</button>
"""
end
defp losing_features_message(:ok), do: nil
defp losing_features_message({:error, {:unavailable_features, features}}) do
features_list_str =
features
|> Enum.map(fn feature_mod -> feature_mod.display_name() end)
|> PlausibleWeb.TextHelpers.pretty_join()
"This plan does not support #{features_list_str}, which you have been using. By subscribing to this plan, you will not have access to #{if length(features) == 1, do: "this feature", else: "these features"}."
end
defp contact_button(assigns) do
~H"""
<.link
href="https://plausible.io/contact"
class={[
"mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 bg-gray-800 hover:bg-gray-700 text-white dark:bg-indigo-600 dark:hover:bg-indigo-500",
@class
]}
>
Contact us
</.link>
"""
end
end

View File

@ -264,8 +264,8 @@ defmodule PlausibleWeb.Components.Billing.Notice do
def growth_grandfathered(assigns) do
~H"""
<div class="mt-8 space-y-3 text-sm leading-6 text-gray-600 text-justify dark:text-gray-100 xl:mt-10">
Your subscription has been grandfathered in at the same rate and terms as when you first joined. If you don't need the "Business" features, you're welcome to stay on this plan. You can adjust the pageview limit or change the billing frequency of this grandfathered plan. If you're interested in business features, you can upgrade to the new "Business" plan.
<div class="mt-8 space-y-3 text-sm leading-6 text-gray-600 text-justify dark:text-gray-100">
Your subscription has been grandfathered in at the same rate and terms as when you first joined. If you don't need the "Business" features, you're welcome to stay on this plan. You can adjust the pageview limit or change the billing frequency of this grandfathered plan. If you're interested in business features, you can upgrade to a "Business" plan.
</div>
"""
end

View File

@ -13,7 +13,8 @@ defmodule PlausibleWeb.Components.Billing.PlanBenefits do
@doc """
This function takes a list of benefits returned by either one of:
* `for_growth/1`
* `for_starter/1`
* `for_growth/2`
* `for_business/2`
* `for_enterprise/1`.
@ -25,9 +26,9 @@ defmodule PlausibleWeb.Components.Billing.PlanBenefits do
"""
def render(assigns) do
~H"""
<ul role="list" class={["mt-8 space-y-3 text-sm leading-6 xl:mt-10", @class]}>
<li :for={benefit <- @benefits} class="flex gap-x-3">
<Heroicons.check class="h-6 w-5 text-indigo-600 dark:text-green-600" />
<ul role="list" class={["mt-8 space-y-1 text-sm leading-6", @class]}>
<li :for={benefit <- @benefits} class="flex gap-x-1">
<Heroicons.check class="shrink-0 h-5 w-5 text-indigo-600 dark:text-green-600" />
{if is_binary(benefit), do: benefit, else: benefit.(assigns)}
</li>
</ul>
@ -35,20 +36,36 @@ defmodule PlausibleWeb.Components.Billing.PlanBenefits do
end
@doc """
This function takes a growth plan and returns a list representing
This function takes a starter plan and returns a list representing
the different benefits a user gets when subscribing to this plan.
"""
def for_growth(plan) do
def for_starter(starter_plan) do
[
team_member_limit_benefit(plan),
site_limit_benefit(plan),
data_retention_benefit(plan),
site_limit_benefit(starter_plan),
data_retention_benefit(starter_plan),
"Intuitive, fast and privacy-friendly dashboard",
"Email/Slack reports",
"Google Analytics import"
]
|> Kernel.++(feature_benefits(plan))
|> Kernel.++(feature_benefits(starter_plan))
|> Kernel.++(["Saved Segments"])
end
@doc """
Returns Growth benefits for the given Growth plan.
A second argument is also required - list of Starter benefits. This
is because we don't want to list the same benefits in both Starter
and Growth. Everything in Starter is also included in Growth.
"""
def for_growth(growth_plan, starter_benefits) do
[
"Everything in Starter",
site_limit_benefit(growth_plan),
team_member_limit_benefit(growth_plan)
]
|> Kernel.++(feature_benefits(growth_plan))
|> Kernel.--(starter_benefits)
|> Enum.filter(& &1)
end
@ -59,15 +76,16 @@ defmodule PlausibleWeb.Components.Billing.PlanBenefits do
is because we don't want to list the same benefits in both Growth
and Business. Everything in Growth is also included in Business.
"""
def for_business(plan, growth_benefits) do
def for_business(plan, growth_benefits, starter_benefits) do
[
"Everything in Growth",
team_member_limit_benefit(plan),
site_limit_benefit(plan),
team_member_limit_benefit(plan),
data_retention_benefit(plan)
]
|> Kernel.++(feature_benefits(plan))
|> Kernel.--(growth_benefits)
|> Kernel.--(starter_benefits)
|> Kernel.++(["Priority support"])
|> Enum.filter(& &1)
end
@ -114,9 +132,8 @@ defmodule PlausibleWeb.Components.Billing.PlanBenefits do
Enum.flat_map(plan.features, fn feature_mod ->
case feature_mod.name() do
:goals -> ["Goals and custom events"]
:teams -> []
:shared_links -> []
:stats_api -> ["Stats API (600 requests per hour)", "Looker Studio Connector"]
:shared_links -> ["Shared Links", "Embedded Dashboards"]
:revenue_goals -> ["Ecommerce revenue attribution"]
_ -> [feature_mod.display_name()]
end
@ -128,7 +145,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBenefits do
<p>
Sites API access for
<.link
class="text-indigo-500 hover:text-indigo-400"
class="text-indigo-500 hover:underline"
href="https://plausible.io/white-label-web-analytics"
>
reselling

View File

@ -7,6 +7,8 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
alias PlausibleWeb.Components.Billing.{PlanBenefits, Notice}
alias Plausible.Billing.{Plan, Quota, Subscription}
@plan_box_price_container_class "relative h-20 pt-4 max-h-20 whitespace-nowrap overflow-hidden"
def standard(assigns) do
highlight =
cond do
@ -15,13 +17,16 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
true -> nil
end
assigns = assign(assigns, :highlight, highlight)
assigns =
assigns
|> assign(:highlight, highlight)
|> assign(:price_container_class, @plan_box_price_container_class)
~H"""
<div
id={"#{@kind}-plan-box"}
class={[
"shadow-lg bg-white rounded-3xl px-6 sm:px-8 py-4 sm:py-6 dark:bg-gray-800",
"shadow-lg border border-gray-200 dark:border-none bg-white rounded-xl px-6 sm:px-4 py-4 sm:py-3 dark:bg-gray-800",
!@highlight && "dark:ring-gray-600",
@highlight && "ring-2 ring-indigo-600 dark:ring-indigo-300"
]}
@ -37,14 +42,16 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
<.pill :if={@highlight} text={@highlight} />
</div>
<div>
<.render_price_info available={@available} {assigns} />
<div class={@price_container_class}>
<.render_price_info available={@available} {assigns} />
</div>
<%= if @available do %>
<.checkout id={"#{@kind}-checkout"} {assigns} />
<% else %>
<.contact_button class="bg-indigo-600 hover:bg-indigo-500 text-white" />
<% end %>
</div>
<%= if @owned && @kind == :growth && @plan_to_render.generation < 4 do %>
<%= if @owned && @kind == :growth && @plan_to_render.generation < 5 do %>
<Notice.growth_grandfathered />
<% else %>
<PlanBenefits.render benefits={@benefits} class="text-gray-600 dark:text-gray-100" />
@ -54,11 +61,13 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
end
def enterprise(assigns) do
assigns = assign(assigns, :price_container_class, @plan_box_price_container_class)
~H"""
<div
id="enterprise-plan-box"
class={[
"rounded-3xl px-6 sm:px-8 py-4 sm:py-6 bg-gray-900 shadow-xl dark:bg-gray-800",
"rounded-xl px-6 sm:px-4 py-4 sm:py-3 bg-gray-900 shadow-xl dark:bg-gray-800",
!@recommended && "dark:ring-gray-600",
@recommended && "ring-4 ring-indigo-500 dark:ring-2 dark:ring-indigo-300"
]}
@ -79,12 +88,11 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
Recommended
</span>
</div>
<p class="mt-6 flex items-baseline gap-x-1">
<span class="text-4xl font-bold tracking-tight text-white dark:text-gray-100">
<div class={@price_container_class}>
<span class="text-3xl lg:text-2xl xl:text-3xl font-bold tracking-tight text-white dark:text-gray-100">
Custom
</span>
</p>
<p class="h-4 mt-1"></p>
</div>
<.contact_button class="" />
<PlanBenefits.render benefits={@benefits} class="text-gray-300 dark:text-gray-100" />
</div>
@ -106,8 +114,8 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
defp render_price_info(%{available: false} = assigns) do
~H"""
<p id={"#{@kind}-custom-price"} class="mt-6 flex items-baseline gap-x-1">
<span class="text-4xl font-bold tracking-tight text-gray-900 dark:text-white">
<p id={"#{@kind}-custom-price"} class="flex items-baseline gap-x-1">
<span class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
Custom
</span>
</p>
@ -117,56 +125,100 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
defp render_price_info(assigns) do
~H"""
<p class="mt-6 flex items-baseline gap-x-1">
<.price_tag
kind={@kind}
selected_interval={@selected_interval}
plan_to_render={@plan_to_render}
/>
</p>
<p class="mt-1 text-xs">+ VAT if applicable</p>
"""
end
defp price_tag(%{plan_to_render: %Plan{monthly_cost: nil}} = assigns) do
~H"""
<span class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100">
N/A
</span>
<.price_tag kind={@kind} selected_interval={@selected_interval} plan_to_render={@plan_to_render} />
<div id={"#{@kind}-vat-notice"} class="absolute top-5 right-0 text-xs text-gray-500">
+ VAT
<span class="hidden sm:inline lg:hidden xl:inline">
if applicable
</span>
</div>
"""
end
defp price_tag(%{selected_interval: :monthly} = assigns) do
monthly_cost =
case assigns.plan_to_render do
%{monthly_cost: nil} -> "N/A"
%{monthly_cost: monthly_cost} -> Plausible.Billing.format_price(monthly_cost)
end
assigns = assign(assigns, :monthly_cost, monthly_cost)
~H"""
<span
id={"#{@kind}-price-tag-amount"}
class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100"
>
{@plan_to_render.monthly_cost |> Plausible.Billing.format_price()}
</span>
<span
id={"#{@kind}-price-tag-interval"}
class="text-sm font-semibold leading-6 text-gray-600 dark:text-gray-500"
>
/month
</span>
<p class="flex items-baseline gap-x-1">
<span
id={"#{@kind}-price-tag-amount"}
class="text-3xl lg:text-2xl xl:text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-100"
>
{@monthly_cost}
</span>
<span
id={"#{@kind}-price-tag-interval"}
class="text-sm font-semibold leading-6 text-gray-600 dark:text-gray-500"
>
/month
</span>
</p>
"""
end
defp price_tag(%{selected_interval: :yearly} = assigns) do
monthly_cost =
case assigns.plan_to_render do
%{monthly_cost: nil} -> "N/A"
%{monthly_cost: monthly_cost} -> Plausible.Billing.format_price(monthly_cost)
end
{yearly_cost, monthly_cost_with_discount} =
case assigns.plan_to_render do
%{yearly_cost: nil} ->
{"N/A", "N/A"}
%{yearly_cost: yearly_cost} ->
{
Plausible.Billing.format_price(yearly_cost),
Plausible.Billing.format_price(Money.div!(yearly_cost, 12))
}
end
assigns =
assigns
|> assign(:monthly_cost, monthly_cost)
|> assign(:yearly_cost, yearly_cost)
|> assign(:monthly_cost_with_discount, monthly_cost_with_discount)
~H"""
<span class="text-2xl font-bold w-max tracking-tight line-through text-gray-500 dark:text-gray-600 mr-1">
{@plan_to_render.monthly_cost |> Money.mult!(12) |> Plausible.Billing.format_price()}
</span>
<span
id={"#{@kind}-price-tag-amount"}
class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100"
>
{@plan_to_render.yearly_cost |> Plausible.Billing.format_price()}
</span>
<span id={"#{@kind}-price-tag-interval"} class="text-sm font-semibold leading-6 text-gray-600">
/year
</span>
<div class="grid grid-cols-[max-content_1fr]">
<span
id={"#{@kind}-price-tag-amount"}
class="text-3xl lg:text-2xl xl:text-3xl font-bold tracking-tight text-gray-900 dark:text-gray-100"
>
{@yearly_cost}
</span>
<span
id={"#{@kind}-price-tag-interval"}
class="text-sm font-semibold leading-6 text-gray-600 pl-1 self-end"
>
/year
</span>
<div class="font-bold tracking-tight text-sm self-center">
<span
id={"#{@kind}-discount-price-tag-strikethrough-amount"}
class="line-through tracking-tight text-gray-500 dark:text-gray-600"
>
{@monthly_cost}
</span>
<span id={"#{@kind}-discount-price-tag-amount"} class="ml-1 text-gray-900 dark:text-gray-100">
{@monthly_cost_with_discount}
</span>
</div>
<span class="text-sm font-semibold text-gray-600 pl-1 self-center">
/month
</span>
</div>
"""
end
@ -237,7 +289,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
</PlausibleWeb.Components.Billing.paddle_button>
<% end %>
<.tooltip :if={@exceeded_plan_limits != [] && @disabled_message}>
<div class="pt-2 text-sm w-full flex items-center text-red-700 dark:text-red-500 justify-center">
<div class="absolute top-0 text-sm w-full flex items-center text-red-700 dark:text-red-500 justify-center">
{@disabled_message}
<Heroicons.information_circle class="hidden sm:block w-5 h-5 sm:ml-2" />
</div>
@ -303,10 +355,16 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
} = _assigns
) do
cond do
from_kind in [:growth, :business] && to_kind == :starter ->
"Downgrade to Starter"
from_kind == :business && to_kind == :growth ->
"Downgrade to Growth"
from_kind == :growth && to_kind == :business ->
from_kind == :starter && to_kind == :growth ->
"Upgrade to Growth"
from_kind in [:starter, :growth] && to_kind == :business ->
"Upgrade to Business"
from_volume == to_volume && from_interval == to_interval ->

View File

@ -483,6 +483,63 @@ defmodule PlausibleWeb.Components.Generic do
"""
end
slot :inner_block, required: true
def accordion_menu(assigns) do
~H"""
<dl class="divide-y divide-gray-200 dark:divide-gray-700">
{render_slot(@inner_block)}
</dl>
"""
end
attr :id, :string, required: true
attr :title, :string, required: true
attr :open_by_default, :boolean, default: false
attr :title_class, :string, default: ""
slot :inner_block, required: true
def accordion_item(assigns) do
~H"""
<div x-data={"{ open: #{@open_by_default}}"} class="py-4">
<dt>
<button
type="button"
class={"flex w-full items-start justify-between text-left #{@title_class}"}
@click="open = !open"
>
<span class="text-base font-semibold">{@title}</span>
<span class="ml-6 flex h-6 items-center">
<svg
x-show="!open"
class="size-5"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" />
</svg>
<svg
x-show="open"
class="size-5"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M18 12H6" />
</svg>
</span>
</button>
</dt>
<dd x-show="open" id={@id} class="mt-2 pr-12 text-sm">
{render_slot(@inner_block)}
</dd>
</div>
"""
end
attr(:rest, :global, include: ~w(fill stroke stroke-width))
attr(:name, :atom, required: true)
attr(:outline, :boolean, default: true)

View File

@ -19,10 +19,19 @@ defmodule PlausibleWeb.BillingController do
def choose_plan(conn, _params) do
team = conn.assigns.current_team
{live_module, hide_header?} =
if FunWithFlags.enabled?(:starter_tier, for: conn.assigns.current_user) do
{PlausibleWeb.Live.ChoosePlan, true}
else
{PlausibleWeb.Live.LegacyChoosePlan, false}
end
if Plausible.Teams.Billing.enterprise_configured?(team) do
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
else
render(conn, "choose_plan.html",
live_module: live_module,
hide_header?: hide_header?,
disable_global_notices?: true,
skip_plausible_tracking: true,
connect_live_socket: true

View File

@ -95,7 +95,7 @@ defmodule PlausibleWeb.Email do
|> render("trial_one_week_reminder.html", user: user, team: team)
end
def trial_upgrade_email(user, team, day, usage, suggested_plan) do
def trial_upgrade_email(user, team, day, usage, suggested_volume) do
base_email()
|> to(user)
|> tag("trial-upgrade-email")
@ -106,7 +106,7 @@ defmodule PlausibleWeb.Email do
day: day,
custom_events: usage.custom_events,
usage: usage.total,
suggested_plan: suggested_plan
suggested_volume: suggested_volume
)
end
@ -157,7 +157,7 @@ defmodule PlausibleWeb.Email do
})
end
def over_limit_email(user, team, usage, suggested_plan) do
def over_limit_email(user, team, usage, suggested_volume) do
priority_email()
|> to(user)
|> tag("over-limit")
@ -166,7 +166,7 @@ defmodule PlausibleWeb.Email do
user: user,
team: team,
usage: usage,
suggested_plan: suggested_plan
suggested_volume: suggested_volume
})
end
@ -183,7 +183,7 @@ defmodule PlausibleWeb.Email do
})
end
def dashboard_locked(user, team, usage, suggested_plan) do
def dashboard_locked(user, team, usage, suggested_volume) do
priority_email()
|> to(user)
|> tag("dashboard-locked")
@ -192,7 +192,7 @@ defmodule PlausibleWeb.Email do
user: user,
team: team,
usage: usage,
suggested_plan: suggested_plan
suggested_volume: suggested_volume
})
end

View File

@ -8,6 +8,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
alias PlausibleWeb.Components.Billing.{PlanBox, PlanBenefits, Notice, PageviewSlider}
alias Plausible.Billing.{Plans, Quota}
alias PlausibleWeb.Router.Helpers, as: Routes
@contact_link "https://plausible.io/contact"
@billing_faq_link "https://plausible.io/docs/billing"
@ -47,9 +48,17 @@ defmodule PlausibleWeb.Live.ChoosePlan do
available_plans: available_plans,
owned_tier: owned_tier
} ->
highest_starter_plan = List.last(available_plans.starter)
highest_growth_plan = List.last(available_plans.growth)
highest_business_plan = List.last(available_plans.business)
Quota.suggest_tier(usage, highest_growth_plan, highest_business_plan, owned_tier)
Quota.suggest_tier(
usage,
highest_starter_plan,
highest_growth_plan,
highest_business_plan,
owned_tier
)
end)
|> assign_new(:available_volumes, fn %{available_plans: available_plans} ->
get_available_volumes(available_plans)
@ -61,7 +70,13 @@ defmodule PlausibleWeb.Live.ChoosePlan do
default_selected_volume(usage.monthly_pageviews, available_volumes)
end)
|> assign_new(:selected_interval, fn %{current_interval: current_interval} ->
current_interval || :monthly
current_interval || :yearly
end)
|> assign_new(:selected_starter_plan, fn %{
available_plans: available_plans,
selected_volume: selected_volume
} ->
get_plan_by_volume(available_plans.starter, selected_volume)
end)
|> assign_new(:selected_growth_plan, fn %{
available_plans: available_plans,
@ -80,24 +95,32 @@ defmodule PlausibleWeb.Live.ChoosePlan do
end
def render(assigns) do
starter_plan_to_render =
assigns.selected_starter_plan || List.last(assigns.available_plans.starter)
growth_plan_to_render =
assigns.selected_growth_plan || List.last(assigns.available_plans.growth)
business_plan_to_render =
assigns.selected_business_plan || List.last(assigns.available_plans.business)
starter_benefits =
PlanBenefits.for_starter(starter_plan_to_render)
growth_benefits =
PlanBenefits.for_growth(growth_plan_to_render)
PlanBenefits.for_growth(growth_plan_to_render, starter_benefits)
business_benefits =
PlanBenefits.for_business(business_plan_to_render, growth_benefits)
PlanBenefits.for_business(business_plan_to_render, growth_benefits, starter_benefits)
enterprise_benefits = PlanBenefits.for_enterprise(business_benefits)
assigns =
assigns
|> assign(:starter_plan_to_render, starter_plan_to_render)
|> assign(:growth_plan_to_render, growth_plan_to_render)
|> assign(:business_plan_to_render, business_plan_to_render)
|> assign(:starter_benefits, starter_benefits)
|> assign(:growth_benefits, growth_benefits)
|> assign(:business_benefits, business_benefits)
|> assign(:enterprise_benefits, enterprise_benefits)
@ -112,21 +135,51 @@ defmodule PlausibleWeb.Live.ChoosePlan do
<Notice.subscription_past_due class="pb-6" subscription={@subscription} />
<Notice.subscription_paused class="pb-6" subscription={@subscription} />
<Notice.upgrade_ineligible :if={not Quota.eligible_for_upgrade?(@usage)} />
<div class="mx-auto max-w-4xl text-center">
<p class="text-4xl font-bold tracking-tight lg:text-5xl">
{if @owned_plan,
do: "Change subscription plan",
else: "Upgrade your account"}
</p>
<div class="mt-6 w-full md:flex">
<a
href={Routes.settings_path(PlausibleWeb.Endpoint, :subscription)}
class="hidden md:flex md:w-1/6 h-max md:mt-2 text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-600 text-sm font-bold gap-1 items-center"
>
<span></span>
<p>Back to Settings</p>
</a>
<div class="md:w-4/6">
<h1 class="mx-auto max-w-4xl text-center text-2xl font-bold tracking-tight lg:text-3xl">
Traffic based plans that match your growth
</h1>
<p class="mx-auto max-w-2xl mt-2 text-center text-gray-600 dark:text-gray-400">
{if @owned_plan,
do: "Change your subscription plan",
else: "Upgrade your trial to a paid plan"}
</p>
</div>
</div>
<div class="mt-12 flex flex-col gap-8 lg:flex-row items-center lg:items-baseline">
<div class="md:hidden mt-6 max-w-md mx-auto">
<a
href={Routes.settings_path(PlausibleWeb.Endpoint, :subscription)}
class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-600 text-sm font-bold"
>
Back to Settings
</a>
</div>
<div class="mt-10 flex flex-col gap-8 lg:flex-row items-center lg:items-baseline">
<.interval_picker selected_interval={@selected_interval} />
<PageviewSlider.render
selected_volume={@selected_volume}
available_volumes={@available_volumes}
/>
</div>
<div class="mt-6 isolate mx-auto grid max-w-md grid-cols-1 gap-8 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<div class="mt-6 isolate mx-auto grid max-w-md grid-cols-1 gap-4 lg:mx-0 lg:max-w-none lg:grid-cols-4">
<PlanBox.standard
kind={:starter}
owned={@owned_tier == :starter}
recommended={@recommended_tier == :starter}
plan_to_render={@starter_plan_to_render}
benefits={@starter_benefits}
available={!!@selected_starter_plan}
{assigns}
/>
<PlanBox.standard
kind={:growth}
owned={@owned_tier == :growth}
@ -150,10 +203,30 @@ defmodule PlausibleWeb.Live.ChoosePlan do
recommended={@recommended_tier == :custom}
/>
</div>
<p class="mx-auto mt-8 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-gray-400">
<.render_usage pageview_usage={@usage.monthly_pageviews} />
</p>
<.pageview_limit_notice :if={!@owned_plan} />
<div class="mt-2 mx-auto max-w-md lg:max-w-3xl">
<.accordion_menu>
<.accordion_item
open_by_default={true}
id="usage"
title="What's my current usage?"
title_class="text-gray-900 dark:text-gray-200"
>
<p class="text-gray-600 dark:text-gray-300">
<.render_usage pageview_usage={@usage.monthly_pageviews} />
</p>
</.accordion_item>
<.accordion_item
id="over-limit"
title="What happens if I go over my monthly pageview limit?"
title_class="text-gray-900 dark:text-gray-200"
>
<p class="text-gray-600 dark:text-gray-300">
You will never be charged extra for an occasional traffic spike. There are no surprise fees and your card will never be charged unexpectedly. If your pageviews exceed your plan for two consecutive months, we will contact you to upgrade to a higher plan for the following month. You will have two weeks to make a decision. You can decide to continue with a higher plan or to cancel your account at that point.
</p>
</.accordion_item>
</.accordion_menu>
</div>
<.help_links />
</div>
</div>
@ -162,19 +235,23 @@ defmodule PlausibleWeb.Live.ChoosePlan do
end
defp render_usage(assigns) do
case assigns.pageview_usage do
%{last_30_days: _} ->
~H"""
You have used
<b><%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_30_days.total) %></b> billable pageviews in the last 30 days
"""
%{last_cycle: _} ->
~H"""
You have used
<b><%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_cycle.total) %></b> billable pageviews in the last billing cycle
"""
end
~H"""
You have used
<span :if={@pageview_usage[:last_30_days]} class="inline">
<b><%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_30_days.total) %></b> billable pageviews in the last 30 days.
</span>
<span :if={@pageview_usage[:last_cycle]} class="inline">
<b>{PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_cycle.total)}</b>
billable pageviews in the last billing cycle.
</span>
Please see your full usage report (including sites and team members) under the
<a
class="text-indigo-600 inline hover:underline"
href={Routes.settings_path(PlausibleWeb.Endpoint, :subscription)}
>
"Subscription" section
</a> in your account settings.
"""
end
def handle_event("set_interval", %{"interval" => interval}, socket) do
@ -201,6 +278,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
{:noreply,
assign(socket,
selected_volume: new_volume,
selected_starter_plan: get_plan_by_volume(available_plans.starter, new_volume),
selected_growth_plan: get_plan_by_volume(available_plans.growth, new_volume),
selected_business_plan: get_plan_by_volume(available_plans.business, new_volume)
)}
@ -266,30 +344,12 @@ defmodule PlausibleWeb.Live.ChoosePlan do
"""
end
defp pageview_limit_notice(assigns) do
~H"""
<div class="mt-12 mx-auto mt-6 max-w-2xl">
<dt>
<p class="w-full text-center text-gray-900 dark:text-gray-100">
<span class="text-center font-semibold leading-7">
What happens if I go over my page views limit?
</span>
</p>
</dt>
<dd class="mt-3">
<div class="text-justify leading-7 block text-gray-600 dark:text-gray-100">
You will never be charged extra for an occasional traffic spike. There are no surprise fees and your card will never be charged unexpectedly. If your page views exceed your plan for two consecutive months, we will contact you to upgrade to a higher plan for the following month. You will have two weeks to make a decision. You can decide to continue with a higher plan or to cancel your account at that point.
</div>
</dd>
</div>
"""
end
defp help_links(assigns) do
~H"""
<div class="mt-8 text-center">
Questions? <a class="text-indigo-600" href={contact_link()}>Contact us</a>
or see <a class="text-indigo-600" href={billing_faq_link()}>billing FAQ</a>
<div class="mt-16 -mb-16 text-center">
Any other questions?
<a class="text-indigo-600 hover:underline" href={contact_link()}>Contact us</a>
or see <a class="text-indigo-600 hover:underline" href={billing_faq_link()}>billing FAQ</a>
</div>
"""
end

View File

@ -0,0 +1,321 @@
defmodule PlausibleWeb.Live.LegacyChoosePlan do
@moduledoc """
[DEPRECATED] This file is essentially a copy of
`PlausibleWeb.Live.ChoosePlan` with the
intent of keeping the old behaviour in place for the users without
the `starter_tier` feature flag enabled.
"""
use PlausibleWeb, :live_view
require Plausible.Billing.Subscription.Status
alias PlausibleWeb.Components.Billing.{
LegacyPlanBox,
LegacyPlanBenefits,
Notice,
PageviewSlider
}
alias Plausible.Billing.{Plans, Quota}
@contact_link "https://plausible.io/contact"
@billing_faq_link "https://plausible.io/docs/billing"
def mount(_params, %{"remote_ip" => remote_ip}, socket) do
socket =
socket
|> assign_new(:pending_ownership_site_ids, fn %{current_user: current_user} ->
Plausible.Teams.Memberships.all_pending_site_transfers(current_user.email)
end)
|> assign_new(:usage, fn %{
current_team: current_team,
pending_ownership_site_ids: pending_ownership_site_ids
} ->
Plausible.Teams.Billing.quota_usage(current_team,
with_features: true,
pending_ownership_site_ids: pending_ownership_site_ids
)
end)
|> assign_new(:subscription, fn %{current_team: current_team} ->
Plausible.Teams.Billing.get_subscription(current_team)
end)
|> assign_new(:owned_plan, fn %{subscription: subscription} ->
Plans.get_regular_plan(subscription, only_non_expired: true)
end)
|> assign_new(:owned_tier, fn %{owned_plan: owned_plan} ->
if owned_plan, do: Map.get(owned_plan, :kind), else: nil
end)
|> assign_new(:current_interval, fn %{subscription: subscription} ->
current_user_subscription_interval(subscription)
end)
|> assign_new(:available_plans, fn %{subscription: subscription} ->
Plans.available_plans_for(subscription,
with_prices: true,
customer_ip: remote_ip,
legacy?: true
)
end)
|> assign_new(:recommended_tier, fn %{
usage: usage,
available_plans: available_plans,
owned_tier: owned_tier
} ->
highest_growth_plan = List.last(available_plans.growth)
highest_business_plan = List.last(available_plans.business)
Quota.legacy_suggest_tier(usage, highest_growth_plan, highest_business_plan, owned_tier)
end)
|> assign_new(:available_volumes, fn %{available_plans: available_plans} ->
get_available_volumes(available_plans)
end)
|> assign_new(:selected_volume, fn %{
usage: usage,
available_volumes: available_volumes
} ->
default_selected_volume(usage.monthly_pageviews, available_volumes)
end)
|> assign_new(:selected_interval, fn %{current_interval: current_interval} ->
current_interval || :monthly
end)
|> assign_new(:selected_growth_plan, fn %{
available_plans: available_plans,
selected_volume: selected_volume
} ->
get_plan_by_volume(available_plans.growth, selected_volume)
end)
|> assign_new(:selected_business_plan, fn %{
available_plans: available_plans,
selected_volume: selected_volume
} ->
get_plan_by_volume(available_plans.business, selected_volume)
end)
{:ok, socket}
end
def render(assigns) do
growth_plan_to_render =
assigns.selected_growth_plan || List.last(assigns.available_plans.growth)
business_plan_to_render =
assigns.selected_business_plan || List.last(assigns.available_plans.business)
growth_benefits =
LegacyPlanBenefits.for_growth(growth_plan_to_render)
business_benefits =
LegacyPlanBenefits.for_business(business_plan_to_render, growth_benefits)
enterprise_benefits = LegacyPlanBenefits.for_enterprise(business_benefits)
assigns =
assigns
|> assign(:growth_plan_to_render, growth_plan_to_render)
|> assign(:business_plan_to_render, business_plan_to_render)
|> assign(:growth_benefits, growth_benefits)
|> assign(:business_benefits, business_benefits)
|> assign(:enterprise_benefits, enterprise_benefits)
~H"""
<div class="pt-1 pb-12 sm:pb-16 text-gray-900 dark:text-gray-100">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<Notice.pending_site_ownerships_notice
class="pb-6"
pending_ownership_count={length(@pending_ownership_site_ids)}
/>
<Notice.subscription_past_due class="pb-6" subscription={@subscription} />
<Notice.subscription_paused class="pb-6" subscription={@subscription} />
<Notice.upgrade_ineligible :if={not Quota.eligible_for_upgrade?(@usage)} />
<div class="mx-auto max-w-4xl text-center">
<p class="text-4xl font-bold tracking-tight lg:text-5xl">
{if @owned_plan,
do: "Change subscription plan",
else: "Upgrade your account"}
</p>
</div>
<div class="mt-12 flex flex-col gap-8 lg:flex-row items-center lg:items-baseline">
<.interval_picker selected_interval={@selected_interval} />
<PageviewSlider.render
selected_volume={@selected_volume}
available_volumes={@available_volumes}
/>
</div>
<div class="mt-6 isolate mx-auto grid max-w-md grid-cols-1 gap-8 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<LegacyPlanBox.standard
kind={:growth}
owned={@owned_tier == :growth}
recommended={@recommended_tier == :growth}
plan_to_render={@growth_plan_to_render}
benefits={@growth_benefits}
available={!!@selected_growth_plan}
{assigns}
/>
<LegacyPlanBox.standard
kind={:business}
owned={@owned_tier == :business}
recommended={@recommended_tier == :business}
plan_to_render={@business_plan_to_render}
benefits={@business_benefits}
available={!!@selected_business_plan}
{assigns}
/>
<LegacyPlanBox.enterprise
benefits={@enterprise_benefits}
recommended={@recommended_tier == :custom}
/>
</div>
<p class="mx-auto mt-8 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-gray-400">
<.render_usage pageview_usage={@usage.monthly_pageviews} />
</p>
<.pageview_limit_notice :if={!@owned_plan} />
<.help_links />
</div>
</div>
<PlausibleWeb.Components.Billing.paddle_script />
"""
end
defp render_usage(assigns) do
case assigns.pageview_usage do
%{last_30_days: _} ->
~H"""
You have used
<b><%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_30_days.total) %></b> billable pageviews in the last 30 days
"""
%{last_cycle: _} ->
~H"""
You have used
<b><%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_cycle.total) %></b> billable pageviews in the last billing cycle
"""
end
end
def handle_event("set_interval", %{"interval" => interval}, socket) do
new_interval =
case interval do
"yearly" -> :yearly
"monthly" -> :monthly
end
{:noreply, assign(socket, selected_interval: new_interval)}
end
def handle_event("slide", %{"slider" => index}, socket) do
index = String.to_integer(index)
%{available_plans: available_plans, available_volumes: available_volumes} = socket.assigns
new_volume =
if index == length(available_volumes) do
:enterprise
else
Enum.at(available_volumes, index)
end
{:noreply,
assign(socket,
selected_volume: new_volume,
selected_growth_plan: get_plan_by_volume(available_plans.growth, new_volume),
selected_business_plan: get_plan_by_volume(available_plans.business, new_volume)
)}
end
defp default_selected_volume(pageview_usage, available_volumes) do
total =
case pageview_usage do
%{last_30_days: usage} -> usage.total
%{last_cycle: usage} -> usage.total
end
Enum.find(available_volumes, &(total < &1)) || :enterprise
end
defp current_user_subscription_interval(subscription) do
case Plans.subscription_interval(subscription) do
"yearly" -> :yearly
"monthly" -> :monthly
_ -> nil
end
end
defp get_plan_by_volume(_, :enterprise), do: nil
defp get_plan_by_volume(plans, volume) do
Enum.find(plans, &(&1.monthly_pageview_limit == volume))
end
defp interval_picker(assigns) do
~H"""
<div class="lg:flex-1 lg:order-3 lg:justify-end flex">
<div class="relative">
<.two_months_free />
<fieldset class="grid grid-cols-2 gap-x-1 rounded-full bg-white dark:bg-gray-700 p-1 text-center text-sm font-semibold leading-5 shadow dark:ring-gray-600">
<label
class={"cursor-pointer rounded-full px-2.5 py-1 text-gray-900 dark:text-white #{if @selected_interval == :monthly, do: "bg-indigo-600 text-white"}"}
phx-click="set_interval"
phx-value-interval="monthly"
>
<input type="radio" name="frequency" value="monthly" class="sr-only" />
<span>Monthly</span>
</label>
<label
class={"cursor-pointer rounded-full px-2.5 py-1 text-gray-900 dark:text-white #{if @selected_interval == :yearly, do: "bg-indigo-600 text-white"}"}
phx-click="set_interval"
phx-value-interval="yearly"
>
<input type="radio" name="frequency" value="yearly" class="sr-only" />
<span>Yearly</span>
</label>
</fieldset>
</div>
</div>
"""
end
def two_months_free(assigns) do
~H"""
<span class="absolute -right-5 -top-4 whitespace-no-wrap w-max px-2.5 py-0.5 rounded-full text-xs font-medium leading-4 bg-yellow-100 border border-yellow-300 text-yellow-700">
2 months free
</span>
"""
end
defp pageview_limit_notice(assigns) do
~H"""
<div class="mt-12 mx-auto mt-6 max-w-2xl">
<dt>
<p class="w-full text-center text-gray-900 dark:text-gray-100">
<span class="text-center font-semibold leading-7">
What happens if I go over my page views limit?
</span>
</p>
</dt>
<dd class="mt-3">
<div class="text-justify leading-7 block text-gray-600 dark:text-gray-100">
You will never be charged extra for an occasional traffic spike. There are no surprise fees and your card will never be charged unexpectedly. If your page views exceed your plan for two consecutive months, we will contact you to upgrade to a higher plan for the following month. You will have two weeks to make a decision. You can decide to continue with a higher plan or to cancel your account at that point.
</div>
</dd>
</div>
"""
end
defp help_links(assigns) do
~H"""
<div class="mt-8 text-center">
Questions? <a class="text-indigo-600" href={contact_link()}>Contact us</a>
or see <a class="text-indigo-600" href={billing_faq_link()}>billing FAQ</a>
</div>
"""
end
defp get_available_volumes(%{business: business_plans, growth: growth_plans}) do
growth_volumes = Enum.map(growth_plans, & &1.monthly_pageview_limit)
business_volumes = Enum.map(business_plans, & &1.monthly_pageview_limit)
(growth_volumes ++ business_volumes)
|> Enum.uniq()
end
defp contact_link(), do: @contact_link
defp billing_faq_link(), do: @billing_faq_link
end

View File

@ -1,4 +1,4 @@
{live_render(@conn, PlausibleWeb.Live.ChoosePlan,
{live_render(@conn, @live_module,
id: "choose-plan",
session: %{"remote_ip" => PlausibleWeb.RemoteIP.get(@conn)}
)}

View File

@ -9,10 +9,10 @@ During the last billing cycle ({PlausibleWeb.TextHelpers.format_date_range(
)}), the usage was {PlausibleWeb.AuthView.delimit_integer(@usage.penultimate_cycle.total)} billable pageviews. Note that billable pageviews include both standard pageviews and custom events. In your
<a href={PlausibleWeb.Router.Helpers.settings_url(PlausibleWeb.Endpoint, :subscription) <> "?__team=#{@team.identifier}"}>account settings</a>, you'll find an overview of your usage and limits.
<br /><br />
<%= if @suggested_plan == :enterprise do %>
<%= if @suggested_volume == :enterprise do %>
Your usage exceeds our standard plans, so please reply back to this email for a tailored quote.
<% else %>
<a href={PlausibleWeb.Router.Helpers.billing_url(PlausibleWeb.Endpoint, :choose_plan) <> "?__team=#{@team.identifier}"}>Click here to upgrade your subscription</a>. We recommend you upgrade to the {@suggested_plan.volume}/mo plan. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire.
<a href={PlausibleWeb.Router.Helpers.billing_url(PlausibleWeb.Endpoint, :choose_plan) <> "?__team=#{@team.identifier}"}>Click here to upgrade your subscription</a>. We recommend you upgrade to the {@suggested_volume}/mo plan. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire.
<br /><br />
If your usage decreases in the future, you can switch to a lower plan at any time. Any credit balance will automatically apply to future payments.
<% end %>

View File

@ -10,10 +10,10 @@ During the last billing cycle ({PlausibleWeb.TextHelpers.format_date_range(
)}), your account used {PlausibleWeb.AuthView.delimit_integer(@usage.penultimate_cycle.total)} billable pageviews. Note that billable pageviews include both standard pageviews and custom events. In your
<a href={plausible_url() <> PlausibleWeb.Router.Helpers.settings_path(PlausibleWeb.Endpoint, :subscription) <> "?__team=#{@team.identifier}"}>account settings</a>, you'll find an overview of your usage and limits.
<br /><br />
<%= if @suggested_plan == :enterprise do %>
<%= if @suggested_volume == :enterprise do %>
Your usage exceeds our standard plans, so please reply back to this email for a tailored quote.
<% else %>
<a href={PlausibleWeb.Router.Helpers.billing_url(PlausibleWeb.Endpoint, :choose_plan) <> "?__team=#{@team.identifier}"}>Click here to upgrade your subscription</a>. We recommend you upgrade to the {@suggested_plan.volume}/mo plan. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire.
<a href={PlausibleWeb.Router.Helpers.billing_url(PlausibleWeb.Endpoint, :choose_plan) <> "?__team=#{@team.identifier}"}>Click here to upgrade your subscription</a>. We recommend you upgrade to the {@suggested_volume}/mo plan. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire.
<br /><br />
If your usage decreases in the future, you can switch to a lower plan at any time. Any credit balance will automatically apply to future payments.
<% end %>

View File

@ -6,10 +6,10 @@ In the last month, your account has used {PlausibleWeb.AuthView.delimit_integer(
" and custom events in total",
else:
""}.
<%= if @suggested_plan == :enterprise do %>
<%= if @suggested_volume == :enterprise do %>
This is more than our standard plans, so please reply back to this email to get a quote for your volume.
<% else %>
Based on that we recommend you select a {@suggested_plan.volume}/mo plan. <br /><br />
Based on that we recommend you select a {@suggested_volume}/mo plan. <br /><br />
<a href={PlausibleWeb.Router.Helpers.billing_url(PlausibleWeb.Endpoint, :choose_plan) <> "?__team=#{@team.identifier}"}>
Upgrade now
</a>

View File

@ -33,7 +33,7 @@
]}
style={if assigns[:background], do: "background-color: #{assigns[:background]}"}
>
<%= if !assigns[:embedded] do %>
<%= if !assigns[:embedded] && !assigns[:hide_header?] do %>
{render("_header.html", assigns)}
<%= if !assigns[:disable_global_notices?] do %>

View File

@ -108,11 +108,11 @@ defmodule Plausible.Workers.CheckUsage do
defp check_regular_subscriber(subscriber, usage_mod) do
case check_pageview_usage_two_cycles(subscriber, usage_mod) do
{:over_limit, pageview_usage} ->
suggested_plan =
Plausible.Billing.Plans.suggest(subscriber, pageview_usage.last_cycle.total)
suggested_volume =
Plausible.Billing.Plans.suggest_volume(subscriber, pageview_usage.last_cycle.total)
for owner <- subscriber.owners ++ subscriber.billing_members do
PlausibleWeb.Email.over_limit_email(owner, subscriber, pageview_usage, suggested_plan)
PlausibleWeb.Email.over_limit_email(owner, subscriber, pageview_usage, suggested_volume)
|> Plausible.Mailer.send()
end

View File

@ -64,20 +64,20 @@ defmodule Plausible.Workers.SendTrialNotifications do
defp send_tomorrow_reminder(users, team) do
usage = Plausible.Teams.Billing.usage_cycle(team, :last_30_days)
suggested_plan = Plausible.Billing.Plans.suggest(team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(team, usage.total)
for user <- users do
PlausibleWeb.Email.trial_upgrade_email(user, team, "tomorrow", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, team, "tomorrow", usage, suggested_volume)
|> Plausible.Mailer.send()
end
end
defp send_today_reminder(users, team) do
usage = Plausible.Teams.Billing.usage_cycle(team, :last_30_days)
suggested_plan = Plausible.Billing.Plans.suggest(team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(team, usage.total)
for user <- users do
PlausibleWeb.Email.trial_upgrade_email(user, team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, team, "today", usage, suggested_volume)
|> Plausible.Mailer.send()
end
end

View File

@ -113,9 +113,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -129,9 +129,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -145,9 +145,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -161,9 +161,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -177,9 +177,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -193,9 +193,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -209,9 +209,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -225,9 +225,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -241,13 +241,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -261,13 +261,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -281,13 +281,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -301,13 +301,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -321,13 +321,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -341,13 +341,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -361,13 +361,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -381,13 +381,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
}

View File

@ -113,9 +113,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -129,9 +129,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -145,9 +145,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -161,9 +161,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -177,9 +177,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -193,9 +193,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -209,9 +209,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -225,9 +225,9 @@
"team_member_limit": 3,
"features": [
"goals",
"site_segments",
"teams",
"shared_links"
"shared_links",
"site_segments"
],
"data_retention_in_years": 3
},
@ -241,13 +241,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -261,13 +261,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -281,13 +281,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -301,13 +301,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -321,13 +321,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -341,13 +341,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -361,13 +361,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
},
@ -381,13 +381,13 @@
"team_member_limit": 10,
"features": [
"goals",
"teams",
"shared_links",
"site_segments",
"props",
"stats_api",
"revenue_goals",
"funnels",
"site_segments",
"teams",
"shared_links"
"funnels"
],
"data_retention_in_years": 5
}

View File

@ -36,21 +36,21 @@ defmodule Plausible.Billing.PlansTest do
|> assert_generation(2)
end
test "growth_plans_for/1 returns v4 plans for expired legacy subscriptions" do
test "growth_plans_for/1 returns latest plans for expired legacy subscriptions" do
new_user()
|> subscribe_to_plan(@v1_plan_id, status: :deleted, next_bill_date: ~D[2023-11-10])
|> team_of(with_subscription?: true)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(4)
|> assert_generation(5)
end
test "growth_plans_for/1 shows v4 plans for everyone else" do
test "growth_plans_for/1 shows latest plans for everyone else" do
new_user(trial_expiry_date: Date.utc_today())
|> team_of(with_subscription?: true)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(4)
|> assert_generation(5)
end
test "growth_plans_for/1 does not return business plans" do
@ -69,7 +69,7 @@ defmodule Plausible.Billing.PlansTest do
|> team_of(with_subscription?: true)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
|> assert_generation(4)
|> assert_generation(5)
end
test "business_plans_for/1 returns v3 business plans for a user on a legacy plan" do
@ -92,13 +92,13 @@ defmodule Plausible.Billing.PlansTest do
assert_generation(business_plans, 3)
end
test "business_plans_for/1 returns v4 plans for invited users with trial_expiry = nil" do
test "business_plans_for/1 returns latest plans for invited users with trial_expiry = nil" do
nil
|> Plans.business_plans_for()
|> assert_generation(4)
|> assert_generation(5)
end
test "business_plans_for/1 returns v4 plans for expired legacy subscriptions" do
test "business_plans_for/1 returns latest plans for expired legacy subscriptions" do
user =
new_user()
|> subscribe_to_plan(@v2_plan_id, status: :deleted, next_bill_date: ~D[2023-11-10])
@ -107,10 +107,10 @@ defmodule Plausible.Billing.PlansTest do
|> team_of(with_subscription?: true)
|> Map.fetch!(:subscription)
|> Plans.business_plans_for()
|> assert_generation(4)
|> assert_generation(5)
end
test "business_plans_for/1 returns v4 business plans for everyone else" do
test "business_plans_for/1 returns latest business plans for everyone else" do
user = new_user(trial_expiry_date: Date.utc_today())
subscription =
@ -121,7 +121,7 @@ defmodule Plausible.Billing.PlansTest do
business_plans = Plans.business_plans_for(subscription)
assert Enum.all?(business_plans, &(&1.kind == :business))
assert_generation(business_plans, 4)
assert_generation(business_plans, 5)
end
test "available_plans returns all plans for user with prices when asked for" do
@ -179,7 +179,7 @@ defmodule Plausible.Billing.PlansTest do
Plausible.Teams.Billing.latest_enterprise_plan_with_price(team, "127.0.0.1")
assert enterprise_plan.paddle_plan_id == "123"
assert price == Money.new(:EUR, "10.0")
assert price == Money.new(:EUR, "123.00")
end
end
@ -214,42 +214,27 @@ defmodule Plausible.Billing.PlansTest do
end
end
describe "suggested_plan/2" do
describe "suggest_volume/2" do
test "returns suggested plan based on usage" do
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
assert %Plausible.Billing.Plan{
monthly_pageview_limit: 100_000,
monthly_cost: nil,
monthly_product_id: "558745",
volume: "100k",
yearly_cost: nil,
yearly_product_id: "590752"
} = Plans.suggest(team, 10_000)
assert %Plausible.Billing.Plan{
monthly_pageview_limit: 200_000,
monthly_cost: nil,
monthly_product_id: "597485",
volume: "200k",
yearly_cost: nil,
yearly_product_id: "597486"
} = Plans.suggest(team, 100_000)
assert Plans.suggest_volume(team, 10_000) == "100k"
assert Plans.suggest_volume(team, 100_000) == "200k"
end
test "returns nil when user has enterprise-level usage" do
test "returns :enterprise when user has enterprise-level usage" do
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
assert :enterprise == Plans.suggest(team, 100_000_000)
assert Plans.suggest_volume(team, 10_000_000) == :enterprise
end
test "returns nil when user is on an enterprise plan" do
test "returns :enterprise when user is on an enterprise plan" do
team =
new_user()
|> subscribe_to_plan(@v1_plan_id)
|> subscribe_to_enterprise_plan(billing_interval: :yearly, subscription?: false)
|> team_of()
assert :enterprise == Plans.suggest(team, 10_000)
assert Plans.suggest_volume(team, 10_000) == :enterprise
end
end
@ -309,7 +294,31 @@ defmodule Plausible.Billing.PlansTest do
"857091",
"857092",
"857093",
"857094"
"857094",
"910414",
"910416",
"910418",
"910420",
"910422",
"910424",
"910426",
"910428",
"910430",
"910432",
"910434",
"910436",
"910438",
"910440",
"910442",
"910444",
"910446",
"910448",
"910450",
"910452",
"910454",
"910456",
"910458",
"910460"
] == Plans.yearly_product_ids()
end
end

View File

@ -17,11 +17,13 @@ defmodule Plausible.Billing.QuotaTest do
@v2_plan_id "654177"
@v3_plan_id "749342"
@v4_1m_plan_id "857101"
@v4_10m_growth_plan_id "857104"
@v4_10m_business_plan_id "857112"
@v5_10m_starter_plan_id "910427"
@v5_10m_growth_plan_id "910443"
@v5_10m_business_plan_id "910459"
@highest_growth_plan Plausible.Billing.Plans.find(@v4_10m_growth_plan_id)
@highest_business_plan Plausible.Billing.Plans.find(@v4_10m_business_plan_id)
@highest_starter_plan Plausible.Billing.Plans.find(@v5_10m_starter_plan_id)
@highest_growth_plan Plausible.Billing.Plans.find(@v5_10m_growth_plan_id)
@highest_business_plan Plausible.Billing.Plans.find(@v5_10m_business_plan_id)
on_ee do
@v3_business_plan_id "857481"
@ -958,7 +960,12 @@ defmodule Plausible.Billing.QuotaTest do
suggested_tier =
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, nil)
|> Quota.suggest_tier(
@highest_starter_plan,
@highest_growth_plan,
@highest_business_plan,
nil
)
assert suggested_tier == nil
end
@ -969,18 +976,44 @@ defmodule Plausible.Billing.QuotaTest do
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Map.merge(%{monthly_pageviews: %{last_30_days: %{total: 12_000_000}}, sites: 1})
|> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, nil)
|> Quota.suggest_tier(
@highest_starter_plan,
@highest_growth_plan,
@highest_business_plan,
nil
)
assert suggested_tier == :custom
end
test "returns :starter if usage within starter limits",
%{team: team} do
suggested_tier =
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Map.put(:sites, 2)
|> Quota.suggest_tier(
@highest_starter_plan,
@highest_growth_plan,
@highest_business_plan,
nil
)
assert suggested_tier == :starter
end
test "returns :growth if usage within growth limits",
%{team: team} do
suggested_tier =
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Map.put(:sites, 1)
|> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, nil)
|> Map.put(:sites, 8)
|> Quota.suggest_tier(
@highest_starter_plan,
@highest_growth_plan,
@highest_business_plan,
nil
)
assert suggested_tier == :growth
end
@ -991,7 +1024,12 @@ defmodule Plausible.Billing.QuotaTest do
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Map.put(:sites, 1)
|> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, :business)
|> Quota.suggest_tier(
@highest_starter_plan,
@highest_growth_plan,
@highest_business_plan,
:business
)
assert suggested_tier == :business
end
@ -1002,7 +1040,12 @@ defmodule Plausible.Billing.QuotaTest do
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Map.merge(%{sites: 1, features: [Plausible.Billing.Feature.Funnels]})
|> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, nil)
|> Quota.suggest_tier(
@highest_starter_plan,
@highest_growth_plan,
@highest_business_plan,
nil
)
assert suggested_tier == :business
end

View File

@ -120,7 +120,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paid",
plan_link: ^plan_link,
plan_label: "10k Plan (€10 monthly)"
plan_label: "10k Plan (€19 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@ -145,7 +145,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paid",
plan_link: ^plan_link,
plan_label: "10k Plan (€100 yearly)"
plan_label: "10k Plan (€190 yearly)"
}} = HelpScout.get_details_for_customer("500")
end
@ -179,7 +179,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paid",
plan_link: _,
plan_label: "1M Enterprise Plan (€10 monthly)"
plan_label: "1M Enterprise Plan (€123 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@ -198,7 +198,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paid",
plan_link: _,
plan_label: "1M Enterprise Plan (€10 yearly)"
plan_label: "1M Enterprise Plan (€123 yearly)"
}} = HelpScout.get_details_for_customer("500")
end
@ -216,7 +216,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Pending cancellation",
plan_link: _,
plan_label: "10k Plan (€10 monthly)"
plan_label: "10k Plan (€19 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@ -235,7 +235,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Canceled",
plan_link: _,
plan_label: "10k Plan (€10 monthly)"
plan_label: "10k Plan (€19 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@ -253,7 +253,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paused",
plan_link: _,
plan_label: "10k Plan (€10 monthly)"
plan_label: "10k Plan (€19 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@ -271,7 +271,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Dashboard locked",
plan_link: _,
plan_label: "10k Plan (€10 monthly)",
plan_label: "10k Plan (€19 monthly)",
sites_count: 1
}} = HelpScout.get_details_for_customer("500")
end

View File

@ -40,7 +40,7 @@ defmodule Plausible.ReleaseTest do
assert stdout =~ "Loading plausible.."
assert stdout =~ "Starting dependencies.."
assert stdout =~ "Starting repos.."
assert stdout =~ "Inserted 54 plans"
assert stdout =~ "Inserted 78 plans"
end
test "ecto_repos sanity check" do

View File

@ -146,7 +146,7 @@ defmodule PlausibleWeb.BillingControllerTest do
assert doc =~ ~r/Up to\s*<b>\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*<b>\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*<b>\s*5k\s*<\/b>\s*hourly api requests/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€10\s*<\/b>\s*/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€123\s*<\/b>\s*/
assert doc =~ "per year"
end
@ -197,7 +197,7 @@ defmodule PlausibleWeb.BillingControllerTest do
assert doc =~ ~r/Up to\s*<b>\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*<b>\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*<b>\s*5k\s*<\/b>\s*hourly api requests/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€10\s*<\/b>\s*/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€123\s*<\/b>\s*/
assert doc =~ "per year"
end
@ -321,7 +321,7 @@ defmodule PlausibleWeb.BillingControllerTest do
assert doc =~ ~r/Up to\s*<b>\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*<b>\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*<b>\s*5k\s*<\/b>\s*hourly api requests/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€10\s*<\/b>\s*/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€123\s*<\/b>\s*/
assert doc =~ "per year"
end

View File

@ -152,7 +152,6 @@ defmodule PlausibleWeb.EmailTest do
team = build(:team, identifier: Ecto.UUID.generate())
penultimate_cycle = Date.range(~D[2023-03-01], ~D[2023-03-31])
last_cycle = Date.range(~D[2023-04-01], ~D[2023-04-30])
suggested_plan = %Plausible.Billing.Plan{volume: "100k"}
usage = %{
penultimate_cycle: %{date_range: penultimate_cycle, total: 12_300},
@ -160,7 +159,7 @@ defmodule PlausibleWeb.EmailTest do
}
%{html_body: html_body, subject: subject} =
PlausibleWeb.Email.over_limit_email(user, team, usage, suggested_plan)
PlausibleWeb.Email.over_limit_email(user, team, usage, "100k")
assert subject == "[Action required] You have outgrown your Plausible subscription tier"
@ -214,7 +213,6 @@ defmodule PlausibleWeb.EmailTest do
team = build(:team, identifier: Ecto.UUID.generate())
penultimate_cycle = Date.range(~D[2023-03-01], ~D[2023-03-31])
last_cycle = Date.range(~D[2023-04-01], ~D[2023-04-30])
suggested_plan = %Plausible.Billing.Plan{volume: "100k"}
usage = %{
penultimate_cycle: %{date_range: penultimate_cycle, total: 12_300},
@ -222,7 +220,7 @@ defmodule PlausibleWeb.EmailTest do
}
%{html_body: html_body, subject: subject} =
PlausibleWeb.Email.dashboard_locked(user, team, usage, suggested_plan)
PlausibleWeb.Email.dashboard_locked(user, team, usage, "100k")
assert subject == "[Action required] Your Plausible dashboard is now locked"

View File

@ -11,9 +11,13 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
@v1_10k_yearly_plan_id "572810"
@v1_50m_yearly_plan_id "650653"
@v2_20m_yearly_plan_id "653258"
@v4_growth_10k_yearly_plan_id "857079"
@v5_starter_5m_monthly_plan_id "910425"
@v4_growth_10k_monthly_plan_id "857097"
@v4_growth_200k_yearly_plan_id "857081"
@v5_growth_10k_yearly_plan_id "910430"
@v5_growth_200k_yearly_plan_id "910434"
@v4_business_5m_monthly_plan_id "857111"
@v5_business_5m_monthly_plan_id "910457"
@v3_business_10k_monthly_plan_id "857481"
@monthly_interval_button ~s/label[phx-click="set_interval"][phx-value-interval="monthly"]/
@ -22,16 +26,32 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
@slider_input ~s/input[name="slider"]/
@slider_value "#slider-value"
@starter_plan_box "#starter-plan-box"
@starter_plan_tooltip "#starter-plan-box .tooltip-content"
@starter_price_tag_amount "#starter-price-tag-amount"
@starter_price_tag_interval "#starter-price-tag-interval"
@starter_discount_price_tag_amount "#starter-discount-price-tag-amount"
@starter_discount_price_tag_strikethrough_amount "#starter-discount-price-tag-strikethrough-amount"
@starter_vat_notice "#starter-vat-notice"
@starter_highlight_pill "#{@starter_plan_box} #highlight-pill"
@starter_checkout_button "#starter-checkout"
@growth_plan_box "#growth-plan-box"
@growth_plan_tooltip "#growth-plan-box .tooltip-content"
@growth_price_tag_amount "#growth-price-tag-amount"
@growth_price_tag_interval "#growth-price-tag-interval"
@growth_discount_price_tag_amount "#growth-discount-price-tag-amount"
@growth_discount_price_tag_strikethrough_amount "#growth-discount-price-tag-strikethrough-amount"
@growth_vat_notice "#growth-vat-notice"
@growth_highlight_pill "#{@growth_plan_box} #highlight-pill"
@growth_checkout_button "#growth-checkout"
@business_plan_box "#business-plan-box"
@business_price_tag_amount "#business-price-tag-amount"
@business_price_tag_interval "#business-price-tag-interval"
@business_discount_price_tag_amount "#business-discount-price-tag-amount"
@business_discount_price_tag_strikethrough_amount "#business-discount-price-tag-strikethrough-amount"
@business_vat_notice "#business-vat-notice"
@business_highlight_pill "#{@business_plan_box} #highlight-pill"
@business_checkout_button "#business-checkout"
@ -46,14 +66,15 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "displays basic page content", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert doc =~ "Upgrade your account"
assert doc =~ "Upgrade your trial"
assert doc =~ "Back to Settings"
assert doc =~ "You have used"
assert doc =~ "<b>0</b>"
assert doc =~ "billable pageviews in the last 30 days"
assert doc =~ "Questions?"
assert doc =~ "What happens if I go over my page views limit?"
assert doc =~ "Any other questions?"
assert doc =~ "What happens if I go over my monthly pageview limit?"
assert doc =~ "Enterprise"
assert doc =~ "+ VAT if applicable"
assert doc =~ "+ VAT"
end
test "does not render any global notices", %{conn: conn} do
@ -64,16 +85,23 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
starter_box = text_of_element(doc, @starter_plan_box)
growth_box = text_of_element(doc, @growth_plan_box)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
assert starter_box =~ "Intuitive, fast and privacy-friendly dashboard"
assert starter_box =~ "Email/Slack reports"
assert starter_box =~ "Google Analytics import"
assert starter_box =~ "Goals and custom events"
assert starter_box =~ "Up to 3 sites"
assert starter_box =~ "3 years of data retention"
assert growth_box =~ "Up to 3 team members"
assert growth_box =~ "Up to 10 sites"
assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
assert growth_box =~ "Email/Slack reports"
assert growth_box =~ "Google Analytics import"
assert growth_box =~ "Goals and custom events"
assert growth_box =~ "Team Accounts"
assert growth_box =~ "Shared Links"
assert growth_box =~ "Shared Segments"
assert business_box =~ "Everything in Growth"
assert business_box =~ "Up to 10 team members"
@ -98,23 +126,24 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"https://plausible.io/white-label-web-analytics"
end
test "default billing interval is monthly, and can switch to yearly", %{conn: conn} do
test "default billing interval is yearly, and can switch to monthly", %{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
assert class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
refute class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
doc = element(lv, @yearly_interval_button) |> render_click()
refute class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
refute class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
doc = element(lv, @monthly_interval_button) |> render_click()
refute class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
assert class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
end
test "default pageview limit is 10k", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @slider_value) == "10k"
assert text_of_element(doc, @growth_price_tag_amount) == "€10"
assert text_of_element(doc, @business_price_tag_amount) == "€90"
assert text_of_element(doc, @starter_price_tag_amount) == "€90"
assert text_of_element(doc, @growth_price_tag_amount) == "€140"
assert text_of_element(doc, @business_price_tag_amount) == "€190"
end
test "pageview slider changes selected volume and prices shown", %{conn: conn} do
@ -122,41 +151,69 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "100k")
assert text_of_element(doc, @slider_value) == "100k"
assert text_of_element(doc, @growth_price_tag_amount) == "€20"
assert text_of_element(doc, @business_price_tag_amount) == "€100"
assert text_of_element(doc, @starter_price_tag_amount) == "€190"
assert text_of_element(doc, @growth_price_tag_amount) == "€290"
assert text_of_element(doc, @business_price_tag_amount) == "€390"
doc = set_slider(lv, "200k")
assert text_of_element(doc, @slider_value) == "200k"
assert text_of_element(doc, @growth_price_tag_amount) == "€30"
assert text_of_element(doc, @business_price_tag_amount) == "€110"
assert text_of_element(doc, @starter_price_tag_amount) == "€290"
assert text_of_element(doc, @growth_price_tag_amount) == "€440"
assert text_of_element(doc, @business_price_tag_amount) == "€590"
doc = set_slider(lv, "500k")
assert text_of_element(doc, @slider_value) == "500k"
assert text_of_element(doc, @growth_price_tag_amount) == "€40"
assert text_of_element(doc, @business_price_tag_amount) == "€120"
assert text_of_element(doc, @starter_price_tag_amount) == "€490"
assert text_of_element(doc, @growth_price_tag_amount) == "€740"
assert text_of_element(doc, @business_price_tag_amount) == "€990"
doc = set_slider(lv, "1M")
assert text_of_element(doc, @slider_value) == "1M"
assert text_of_element(doc, @growth_price_tag_amount) == "€50"
assert text_of_element(doc, @business_price_tag_amount) == "€130"
assert text_of_element(doc, @starter_price_tag_amount) == "€690"
assert text_of_element(doc, @growth_price_tag_amount) == "€1,040"
assert text_of_element(doc, @business_price_tag_amount) == "€1,390"
doc = set_slider(lv, "2M")
assert text_of_element(doc, @slider_value) == "2M"
assert text_of_element(doc, @growth_price_tag_amount) == "€60"
assert text_of_element(doc, @business_price_tag_amount) == "€140"
assert text_of_element(doc, @starter_price_tag_amount) == "€890"
assert text_of_element(doc, @growth_price_tag_amount) == "€1,340"
assert text_of_element(doc, @business_price_tag_amount) == "€1,790"
doc = set_slider(lv, "5M")
assert text_of_element(doc, @slider_value) == "5M"
assert text_of_element(doc, @growth_price_tag_amount) == "€70"
assert text_of_element(doc, @business_price_tag_amount) == "€150"
assert text_of_element(doc, @starter_price_tag_amount) == "€1,290"
assert text_of_element(doc, @growth_price_tag_amount) == "€1,940"
assert text_of_element(doc, @business_price_tag_amount) == "€2,590"
doc = set_slider(lv, "10M")
assert text_of_element(doc, @slider_value) == "10M"
assert text_of_element(doc, @growth_price_tag_amount) == "€80"
assert text_of_element(doc, @business_price_tag_amount) == "€160"
assert text_of_element(doc, @starter_price_tag_amount) == "€1,690"
assert text_of_element(doc, @growth_price_tag_amount) == "€2,540"
assert text_of_element(doc, @business_price_tag_amount) == "€3,390"
end
test "renders contact links for business and growth tiers when enterprise-level volume selected",
test "displays monthly discount for yearly plans", %{conn: conn} do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "200k")
assert text_of_element(doc, @starter_price_tag_amount) == "€290"
assert text_of_element(doc, @starter_discount_price_tag_amount) == "€24.17"
assert text_of_element(doc, @starter_discount_price_tag_strikethrough_amount) == "€29"
assert text_of_element(doc, @starter_vat_notice) == "+ VAT if applicable"
assert text_of_element(doc, @growth_price_tag_amount) == "€440"
assert text_of_element(doc, @growth_discount_price_tag_amount) == "€36.67"
assert text_of_element(doc, @growth_discount_price_tag_strikethrough_amount) == "€44"
assert text_of_element(doc, @growth_vat_notice) == "+ VAT if applicable"
assert text_of_element(doc, @business_price_tag_amount) == "€590"
assert text_of_element(doc, @business_discount_price_tag_amount) == "€49.17"
assert text_of_element(doc, @business_discount_price_tag_strikethrough_amount) == "€59"
assert text_of_element(doc, @business_vat_notice) == "+ VAT if applicable"
end
test "renders contact links for all tiers when enterprise-level volume selected",
%{
conn: conn
} do
@ -164,6 +221,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "10M+")
assert text_of_element(doc, "#starter-custom-price") =~ "Custom"
assert text_of_element(doc, @starter_plan_box) =~ "Contact us"
assert text_of_element(doc, "#growth-custom-price") =~ "Custom"
assert text_of_element(doc, @growth_plan_box) =~ "Contact us"
assert text_of_element(doc, "#business-custom-price") =~ "Custom"
@ -171,28 +230,36 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "10M")
refute text_of_element(doc, "#starter-custom-price") =~ "Custom"
refute text_of_element(doc, @starter_plan_box) =~ "Contact us"
refute text_of_element(doc, "#growth-custom-price") =~ "Custom"
refute text_of_element(doc, @growth_plan_box) =~ "Contact us"
refute text_of_element(doc, "#business-custom-price") =~ "Custom"
refute text_of_element(doc, @business_plan_box) =~ "Contact us"
end
test "switching billing interval changes business and growth prices", %{conn: conn} do
test "switching billing interval changes prices", %{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
assert text_of_element(doc, @growth_price_tag_amount) == "€10"
assert text_of_element(doc, @growth_price_tag_interval) == "/month"
assert text_of_element(doc, @starter_price_tag_amount) == "€90"
assert text_of_element(doc, @starter_price_tag_interval) == "/year"
assert text_of_element(doc, @business_price_tag_amount) == "€90"
assert text_of_element(doc, @business_price_tag_interval) == "/month"
doc = element(lv, @yearly_interval_button) |> render_click()
assert text_of_element(doc, @growth_price_tag_amount) == "€100"
assert text_of_element(doc, @growth_price_tag_amount) == "€140"
assert text_of_element(doc, @growth_price_tag_interval) == "/year"
assert text_of_element(doc, @business_price_tag_amount) == "900"
assert text_of_element(doc, @business_price_tag_amount) == "€190"
assert text_of_element(doc, @business_price_tag_interval) == "/year"
doc = element(lv, @monthly_interval_button) |> render_click()
assert text_of_element(doc, @starter_price_tag_amount) == "€9"
assert text_of_element(doc, @starter_price_tag_interval) == "/month"
assert text_of_element(doc, @growth_price_tag_amount) == "€14"
assert text_of_element(doc, @growth_price_tag_interval) == "/month"
assert text_of_element(doc, @business_price_tag_amount) == "€19"
assert text_of_element(doc, @business_price_tag_interval) == "/month"
end
test "checkout buttons are 'paddle buttons' with dynamic onclick attribute", %{
@ -209,7 +276,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"disableLogout" => true,
"email" => user.email,
"passthrough" => "ee:true;user:#{user.id};team:#{team.id}",
"product" => @v4_growth_200k_yearly_plan_id,
"product" => @v5_growth_200k_yearly_plan_id,
"success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success),
"theme" => "none"
} == get_paddle_checkout_params(find(doc, @growth_checkout_button))
@ -217,8 +284,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
set_slider(lv, "5M")
doc = element(lv, @monthly_interval_button) |> render_click()
assert get_paddle_checkout_params(find(doc, @starter_checkout_button))["product"] ==
@v5_starter_5m_monthly_plan_id
assert get_paddle_checkout_params(find(doc, @business_checkout_button))["product"] ==
@v4_business_5m_monthly_plan_id
@v5_business_5m_monthly_plan_id
end
test "warns about losing access to a feature", %{conn: conn, site: site} do
@ -230,18 +300,30 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"if (confirm(\"This plan does not support Custom Properties, which you have been using. By subscribing to this plan, you will not have access to this feature.\")) {Paddle.Checkout.open"
end
test "recommends Growth tier when no premium features were used", %{conn: conn} do
test "recommends Starter", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @starter_highlight_pill) == "Recommended"
refute element_exists?(doc, @growth_highlight_pill)
refute element_exists?(doc, @business_highlight_pill)
end
test "recommends Growth", %{conn: conn, site: site} do
for _ <- 1..3, do: add_guest(site, role: :viewer)
{:ok, _lv, doc} = get_liveview(conn)
refute element_exists?(doc, @starter_highlight_pill)
assert text_of_element(doc, @growth_highlight_pill) == "Recommended"
refute element_exists?(doc, @business_highlight_pill)
end
test "recommends Business when Revenue Goals used during trial", %{conn: conn, site: site} do
test "recommends Business", %{conn: conn, site: site} do
insert(:goal, site: site, currency: :USD, event_name: "Purchase")
{:ok, _lv, doc} = get_liveview(conn)
refute element_exists?(doc, @starter_highlight_pill)
assert text_of_element(doc, @business_highlight_pill) == "Recommended"
refute element_exists?(doc, @growth_highlight_pill)
end
@ -308,6 +390,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "100k")
refute class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
refute element_exists?(doc, @growth_plan_tooltip)
@ -317,6 +400,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "100k")
assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
@ -336,6 +420,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "10k")
refute class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
@ -344,6 +429,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "10k")
assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
end
@ -363,6 +449,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "10k")
refute class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
@ -371,20 +458,21 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "10k")
assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
end
end
describe "for a user with an active v4 growth subscription plan" do
setup [:create_user, :create_site, :log_in, :subscribe_v4_growth]
describe "for a user with an active v5 growth subscription plan" do
setup [:create_user, :create_site, :log_in, :subscribe_v5_growth]
test "displays basic page content", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert doc =~ "Change subscription plan"
assert doc =~ "Questions?"
refute doc =~ "What happens if I go over my page views limit?"
assert doc =~ "Change your subscription plan"
assert doc =~ "Any other questions?"
assert doc =~ "What happens if I go over my monthly pageview limit?"
end
test "does not render any global notices", %{conn: conn} do
@ -395,17 +483,23 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
starter_box = text_of_element(doc, @starter_plan_box)
growth_box = text_of_element(doc, @growth_plan_box)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
assert starter_box =~ "Intuitive, fast and privacy-friendly dashboard"
assert starter_box =~ "Email/Slack reports"
assert starter_box =~ "Google Analytics import"
assert starter_box =~ "Goals and custom events"
assert starter_box =~ "Up to 3 sites"
assert starter_box =~ "3 years of data retention"
assert growth_box =~ "Up to 3 team members"
assert growth_box =~ "Up to 10 sites"
assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
assert growth_box =~ "Email/Slack reports"
assert growth_box =~ "Google Analytics import"
assert growth_box =~ "Goals and custom events"
assert growth_box =~ "3 years of data retention"
assert growth_box =~ "Team Accounts"
assert growth_box =~ "Shared Links"
assert growth_box =~ "Shared Segments"
assert business_box =~ "Everything in Growth"
assert business_box =~ "Up to 10 team members"
@ -475,6 +569,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
check_notice_titles(doc, [Billing.pending_site_ownerships_notice_title()])
assert doc =~ "Your account has been invited to become the owner of a site"
assert text_of_element(doc, @starter_plan_tooltip) ==
"Your usage exceeds the following limit(s): Team member limit"
assert text_of_element(doc, @growth_plan_tooltip) ==
"Your usage exceeds the following limit(s): Team member limit"
@ -505,9 +602,17 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
refute element_exists?(doc, @growth_highlight_pill)
end
test "gets default selected interval from current subscription plan", %{conn: conn} do
test "gets default selected interval from current subscription plan", %{
conn: conn,
user: user
} do
{:ok, _lv, doc} = get_liveview(conn)
assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
subscribe_to_plan(user, @v4_growth_10k_monthly_plan_id)
{:ok, _lv, doc} = get_liveview(conn)
assert class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
end
test "sets pageview slider according to last cycle usage", %{conn: conn} do
@ -540,22 +645,27 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "200k")
assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Currently on this plan"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400"
assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400"
doc = element(lv, @monthly_interval_button) |> render_click()
assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Change billing interval"
assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
doc = set_slider(lv, "1M")
assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Upgrade"
assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
doc = set_slider(lv, "100k")
assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade"
assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
end
@ -568,15 +678,20 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
growth_checkout_button = find(doc, @growth_checkout_button)
assert text_of_attr(growth_checkout_button, "onclick") =~
"if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v4_growth_10k_yearly_plan_id)}'}"
"if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v5_growth_10k_yearly_plan_id)}'}"
set_slider(lv, "5M")
doc = element(lv, @monthly_interval_button) |> render_click()
starter_checkout_button = find(doc, @starter_checkout_button)
assert text_of_attr(starter_checkout_button, "onclick") =~
"if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v5_starter_5m_monthly_plan_id)}'}"
business_checkout_button = find(doc, @business_checkout_button)
assert text_of_attr(business_checkout_button, "onclick") =~
"if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v4_business_5m_monthly_plan_id)}'}"
"if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v5_business_5m_monthly_plan_id)}'}"
end
end
@ -607,6 +722,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert class =~ "ring-indigo-600"
assert text_of_element(doc, @business_highlight_pill) == "Current"
refute element_exists?(doc, @starter_highlight_pill)
refute element_exists?(doc, @growth_highlight_pill)
end
@ -631,6 +747,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @enterprise_highlight_pill) == "Recommended"
refute element_exists?(doc, @starter_highlight_pill)
refute element_exists?(doc, @business_highlight_pill)
refute element_exists?(doc, @growth_highlight_pill)
end
@ -640,23 +757,29 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "5M")
assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
assert text_of_element(doc, @business_checkout_button) == "Currently on this plan"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none bg-gray-400"
doc = element(lv, @yearly_interval_button) |> render_click()
assert text_of_element(doc, @business_checkout_button) == "Change billing interval"
assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
assert text_of_element(doc, @business_checkout_button) == "Change billing interval"
doc = set_slider(lv, "10M")
assert text_of_element(doc, @business_checkout_button) == "Upgrade"
assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
assert text_of_element(doc, @business_checkout_button) == "Upgrade"
doc = set_slider(lv, "100k")
assert text_of_element(doc, @business_checkout_button) == "Downgrade"
assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
assert text_of_element(doc, @business_checkout_button) == "Downgrade"
end
test "checkout is disabled when team member usage exceeds rendered plan limit", %{
@ -668,6 +791,12 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @starter_plan_box) =~ "Your usage exceeds this plan"
assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert text_of_element(doc, @starter_plan_tooltip) ==
"Your usage exceeds the following limit(s): Team member limit"
assert text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
@ -683,6 +812,12 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @starter_plan_box) =~ "Your usage exceeds this plan"
assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert text_of_element(doc, @starter_plan_tooltip) ==
"Your usage exceeds the following limit(s): Site limit"
assert text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
@ -701,6 +836,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @starter_plan_tooltip) =~ "Team member limit"
assert text_of_element(doc, @starter_plan_tooltip) =~ "Site limit"
assert text_of_element(doc, @growth_plan_tooltip) =~ "Team member limit"
assert text_of_element(doc, @growth_plan_tooltip) =~ "Site limit"
end
@ -757,16 +895,23 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
starter_box = text_of_element(doc, @starter_plan_box)
growth_box = text_of_element(doc, @growth_plan_box)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
assert starter_box =~ "Intuitive, fast and privacy-friendly dashboard"
assert starter_box =~ "Email/Slack reports"
assert starter_box =~ "Google Analytics import"
assert starter_box =~ "Goals and custom events"
assert starter_box =~ "Up to 3 sites"
assert starter_box =~ "3 years of data retention"
assert growth_box =~ "Up to 3 team members"
assert growth_box =~ "Up to 10 sites"
assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
assert growth_box =~ "Email/Slack reports"
assert growth_box =~ "Google Analytics import"
assert growth_box =~ "Goals and custom events"
assert growth_box =~ "Team Accounts"
assert growth_box =~ "Shared Links"
assert growth_box =~ "Shared Segments"
assert business_box =~ "Everything in Growth"
assert business_box =~ "Unlimited team members"
@ -824,6 +969,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert text_of_element(doc, "#{@growth_checkout_button} + div") =~
"Please update your billing details first"
assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none bg-gray-400"
assert text_of_element(doc, "#{@starter_checkout_button} + div") =~
"Please update your billing details first"
end
end
@ -858,6 +1008,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert text_of_element(doc, "#{@growth_checkout_button} + div") =~
"Please update your billing details first"
assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none bg-gray-400"
assert text_of_element(doc, "#{@starter_checkout_button} + div") =~
"Please update your billing details first"
end
end
@ -871,6 +1026,10 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "checkout buttons are paddle buttons", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_attr(find(doc, @starter_checkout_button), "onclick") =~
"Paddle.Checkout.open"
assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~ "Paddle.Checkout.open"
assert text_of_attr(find(doc, @business_checkout_button), "onclick") =~
@ -909,7 +1068,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "highlights recommended tier", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @growth_highlight_pill) == "Recommended"
assert text_of_element(doc, @starter_highlight_pill) == "Recommended"
refute text_of_element(doc, @growth_highlight_pill) == "Recommended"
refute text_of_element(doc, @business_highlight_pill) == "Recommended"
end
end
@ -923,7 +1083,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
check_notice_titles(doc, [])
end
test "on a 50M v1 plan, Growth tiers are available at 20M, 50M, 50M+, but Business tiers are not",
test "on a 50M v1 plan, Growth plans are available at 20M, 50M, 50M+, but Starter and Business plans are not",
%{conn: conn, user: user} do
create_subscription_for(user, paddle_plan_id: @v1_50m_yearly_plan_id)
@ -931,23 +1091,27 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, 8)
assert text_of_element(doc, @slider_value) == "20M"
assert text_of_element(doc, @starter_plan_box) =~ "Contact us"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
assert text_of_element(doc, @growth_price_tag_amount) == "900"
assert text_of_element(doc, @growth_price_tag_amount) == "1,800"
assert text_of_element(doc, @growth_price_tag_interval) == "/year"
doc = set_slider(lv, 9)
assert text_of_element(doc, @slider_value) == "50M"
assert text_of_element(doc, @starter_plan_box) =~ "Contact us"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
assert text_of_element(doc, @growth_price_tag_amount) == "1,000"
assert text_of_element(doc, @growth_price_tag_amount) == "2,640"
assert text_of_element(doc, @growth_price_tag_interval) == "/year"
doc = set_slider(lv, 10)
assert text_of_element(doc, @slider_value) == "50M+"
assert text_of_element(doc, @starter_plan_box) =~ "Contact us"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
assert text_of_element(doc, @growth_plan_box) =~ "Contact us"
doc = set_slider(lv, 7)
assert text_of_element(doc, @slider_value) == "10M"
refute text_of_element(doc, @starter_plan_box) =~ "Contact us"
refute text_of_element(doc, @business_plan_box) =~ "Contact us"
refute text_of_element(doc, @growth_plan_box) =~ "Contact us"
end
@ -960,13 +1124,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, 8)
assert text_of_element(doc, @slider_value) == "20M"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
assert text_of_element(doc, @growth_price_tag_amount) == "€900"
assert text_of_element(doc, @growth_price_tag_amount) == "€2,250"
assert text_of_element(doc, @growth_price_tag_interval) == "/year"
doc = set_slider(lv, 9)
assert text_of_element(doc, @slider_value) == "20M+"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
assert text_of_element(doc, @growth_plan_box) =~ "Contact us"
end
end
@ -1000,12 +1162,20 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
refute growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
end
test "displays business and enterprise plan benefits", %{conn: conn} do
test "displays Starter, Business and Enterprise benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
starter_box = text_of_element(doc, @starter_plan_box)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
assert starter_box =~ "Intuitive, fast and privacy-friendly dashboard"
assert starter_box =~ "Email/Slack reports"
assert starter_box =~ "Google Analytics import"
assert starter_box =~ "Goals and custom events"
assert starter_box =~ "Up to 3 sites"
assert starter_box =~ "3 years of data retention"
assert business_box =~ "Everything in Growth"
assert business_box =~ "Funnels"
assert business_box =~ "Ecommerce revenue attribution"
@ -1053,6 +1223,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "100k")
refute class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
end
@ -1061,9 +1232,10 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
describe "for a free_10k subscription" do
setup [:create_user, :create_site, :log_in, :subscribe_free_10k]
test "recommends growth tier when no premium features used", %{conn: conn} do
test "recommends starter tier", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert element_exists?(doc, @growth_highlight_pill)
assert element_exists?(doc, @starter_highlight_pill)
refute element_exists?(doc, @growth_highlight_pill)
refute element_exists?(doc, @business_highlight_pill)
end
@ -1074,6 +1246,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert text_of_element(doc, @business_plan_box) =~ "Recommended"
refute text_of_element(doc, @growth_plan_box) =~ "Recommended"
refute text_of_element(doc, @starter_plan_box) =~ "Recommended"
end
test "renders Paddle upgrade buttons", %{conn: conn, user: user} do
@ -1087,7 +1260,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"disableLogout" => true,
"email" => user.email,
"passthrough" => "ee:true;user:#{user.id};team:#{team.id}",
"product" => @v4_growth_200k_yearly_plan_id,
"product" => @v5_growth_200k_yearly_plan_id,
"success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success),
"theme" => "none"
} == get_paddle_checkout_params(find(doc, @growth_checkout_button))
@ -1103,6 +1276,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
check_notice_titles(doc, [Billing.upgrade_ineligible_notice_title()])
assert text_of_element(doc, "#upgrade-eligible-notice") =~ "You cannot start a subscription"
assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
end
@ -1127,9 +1301,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "allows to subscribe", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @starter_plan_box) =~ "Recommended"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
assert text_of_element(doc, @growth_plan_box) =~ "Recommended"
end
end
@ -1155,8 +1329,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
end)
end
defp subscribe_v4_growth(%{user: user}) do
create_subscription_for(user, paddle_plan_id: @v4_growth_200k_yearly_plan_id)
defp subscribe_v5_growth(%{user: user}) do
create_subscription_for(user, paddle_plan_id: @v5_growth_200k_yearly_plan_id)
end
defp subscribe_v4_business(%{user: user}) do

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,9 @@ defmodule Plausible.Billing.DevPaddleApiMock do
@prices_file_path Application.app_dir(:plausible, ["priv", "plan_prices.json"])
@prices File.read!(@prices_file_path) |> Jason.decode!()
# https://hexdocs.pm/elixir/1.15/Module.html#module-external_resource
@external_resource @prices_file_path
def all_prices() do
enterprise_plan_prices =
Repo.all(from p in EnterprisePlan, select: {p.paddle_plan_id, 123})

View File

@ -1,4 +1,6 @@
defmodule Plausible.PaddleApi.Mock do
defmodule Plausible.Billing.TestPaddleApiMock do
@moduledoc false
def get_subscription(_) do
{:ok,
%{
@ -72,20 +74,7 @@ defmodule Plausible.PaddleApi.Mock do
end
end
# to give a reasonable testing structure for monthly and yearly plan
# prices, this function returns prices with the following logic:
# 10, 100, 20, 200, 30, 300, ...and so on.
def fetch_prices([_ | _] = product_ids, _customer_ip) do
{prices, _index} =
Enum.reduce(product_ids, {%{}, 1}, fn p, {acc, i} ->
price =
if rem(i, 2) == 1,
do: ceil(i / 2.0) * 10.0,
else: ceil(i / 2.0) * 100.0
{Map.put(acc, p, Money.from_float!(:EUR, price)), i + 1}
end)
{:ok, prices}
def fetch_prices(product_ids, customer_ip) do
Plausible.Billing.DevPaddleApiMock.fetch_prices(product_ids, customer_ip)
end
end

View File

@ -8,6 +8,7 @@ Application.ensure_all_started(:double)
FunWithFlags.enable(:channels)
FunWithFlags.enable(:scroll_depth)
FunWithFlags.enable(:starter_tier)
Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, :manual)

View File

@ -77,7 +77,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 1))
site = new_site(owner: user)
usage = %{total: 3, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
populate_stats(site, [
build(:pageview),
@ -88,7 +88,13 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
perform_job(SendTrialNotifications, %{})
assert_delivered_email(
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "tomorrow", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(
user,
site.team,
"tomorrow",
usage,
suggested_volume
)
)
end
@ -96,7 +102,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user(trial_expiry_date: Date.utc_today())
site = new_site(owner: user)
usage = %{total: 3, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
populate_stats(site, [
build(:pageview),
@ -107,7 +113,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
perform_job(SendTrialNotifications, %{})
assert_delivered_email(
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
)
end
@ -115,10 +121,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user(trial_expiry_date: Date.utc_today())
site = new_site(owner: user)
usage = %{total: 9_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~
"In the last month, your account has used 9,000 billable pageviews."
@ -128,10 +134,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user(trial_expiry_date: Date.utc_today())
site = new_site(owner: user)
usage = %{total: 9_100, custom_events: 100}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~
"In the last month, your account has used 9,100 billable pageviews and custom events in total."
@ -175,10 +181,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 9_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "we recommend you select a 10k/mo plan."
end
@ -187,10 +193,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 90_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "we recommend you select a 100k/mo plan."
end
@ -199,10 +205,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 180_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "we recommend you select a 200k/mo plan."
end
@ -211,10 +217,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 450_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "we recommend you select a 500k/mo plan."
end
@ -223,10 +229,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 900_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "we recommend you select a 1M/mo plan."
end
@ -235,10 +241,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 1_800_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "we recommend you select a 2M/mo plan."
end
@ -247,10 +253,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 4_500_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "we recommend you select a 5M/mo plan."
end
@ -259,10 +265,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 9_000_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "we recommend you select a 10M/mo plan."
end
@ -271,10 +277,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = new_user()
site = new_site(owner: user)
usage = %{total: 20_000_000, custom_events: 0}
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "please reply back to this email to get a quote for your volume"
end
@ -284,10 +290,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
site = new_site(owner: user)
usage = %{total: 10_000, custom_events: 0}
subscribe_to_enterprise_plan(user, paddle_plan_id: "enterprise-plan-id")
suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total)
suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total)
email =
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan)
PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume)
assert email.html_body =~ "please reply back to this email to get a quote for your volume"
end