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:
parent
97449613e1
commit
2dd144bf85
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)}
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue