List plan benefits on the new upgrade page (#3444)

* change team member limits for new v4 plans

* duplicate business plans with unlimited team members

We need to do this because we want grandfathered users to have unlimited
team members on business plans as well. Otherwise we'd have to build
overrides on the subscription level when checking the limit.

* refactor generating plan structs

* move Plan module into a separate file

* remove not needed conditions

* add generation field to plans

* sync the sanbox plan limits and features with plan generations

* implement displaying plan benefits

* add grandfathering notice

* plug in the real v3 business plan IDs

* optimize N/A text color for darkmode

* use String.to_existing_atom instead

Co-authored-by: Vini Brasil <vini@hey.com>

* Remove the unnecessary part of a comment

Co-authored-by: Vini Brasil <vini@hey.com>

* make the Plan.new function simpler

* use exlamation marks

---------

Co-authored-by: Vini Brasil <vini@hey.com>
This commit is contained in:
RobertJoonas 2023-10-23 19:42:00 +03:00 committed by GitHub
parent 957138a8ec
commit 2ada3d700f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 639 additions and 185 deletions

View File

@ -0,0 +1,95 @@
defmodule Plausible.Billing.Plan do
@moduledoc false
@derive Jason.Encoder
@enforce_keys ~w(kind generation site_limit monthly_pageview_limit team_member_limit features volume monthly_product_id yearly_product_id)a
defstruct @enforce_keys ++ [:monthly_cost, :yearly_cost]
@type t() ::
%__MODULE__{
kind: atom(),
generation: non_neg_integer(),
monthly_pageview_limit: non_neg_integer(),
site_limit: non_neg_integer(),
team_member_limit: non_neg_integer() | :unlimited,
volume: String.t(),
monthly_cost: Money.t() | nil,
yearly_cost: Money.t() | nil,
monthly_product_id: String.t() | nil,
yearly_product_id: String.t() | nil,
features: [atom()]
}
| :enterprise
def build!(raw_params, file_name) when is_map(raw_params) do
raw_params
|> put_kind()
|> put_generation()
|> put_volume()
|> put_team_member_limit!(file_name)
|> put_features!(file_name)
|> new!()
end
defp new!(params) do
struct!(__MODULE__, params)
end
defp put_kind(params) do
Map.put(params, :kind, String.to_existing_atom(params.kind))
end
# Due to grandfathering, we sometimes need to check the "generation"
# (e.g. v1, v2, etc...) of a user's subscription plan. For instance,
# on prod, the users subscribed to a v2 plan are only supposed to
# see v2 plans when they go to the upgrade page.
#
# In the `dev` environment though, "sandbox" plans are used, which
# unlike production plans, contain multiple generations of plans in
# the same file for testing purposes.
defp put_generation(params) do
Map.put(params, :generation, params.generation)
end
defp put_volume(params) do
volume =
params.monthly_pageview_limit
|> PlausibleWeb.StatsView.large_number_format()
Map.put(params, :volume, volume)
end
defp put_team_member_limit!(params, file_name) do
team_member_limit =
case params.team_member_limit do
number when is_integer(number) ->
number
"unlimited" ->
:unlimited
other ->
raise ArgumentError,
"Failed to parse team member limit #{inspect(other)} from #{file_name}.json"
end
Map.put(params, :team_member_limit, team_member_limit)
end
defp put_features!(params, file_name) do
features =
Plausible.Billing.Feature.list()
|> Enum.filter(fn module ->
to_string(module.name()) in params.features
end)
if length(features) == length(params.features) do
Map.put(params, :features, features)
else
raise(
ArgumentError,
"Unrecognized feature(s) in #{inspect(params.features)} (#{file_name}.json)"
)
end
end
end

View File

@ -1,30 +1,3 @@
defmodule Plausible.Billing.Plan do
@moduledoc false
@derive Jason.Encoder
@enforce_keys ~w(kind site_limit monthly_pageview_limit team_member_limit features volume monthly_product_id yearly_product_id)a
defstruct @enforce_keys ++ [:monthly_cost, :yearly_cost]
@type t() ::
%__MODULE__{
kind: atom(),
monthly_pageview_limit: non_neg_integer(),
site_limit: non_neg_integer(),
team_member_limit: non_neg_integer() | :unlimited,
volume: String.t(),
monthly_cost: Money.t() | nil,
yearly_cost: Money.t() | nil,
monthly_product_id: String.t() | nil,
yearly_product_id: String.t() | nil,
features: [atom()]
}
| :enterprise
def new(params) when is_map(params) do
struct!(__MODULE__, params)
end
end
defmodule Plausible.Billing.Plans do
alias Plausible.Billing.Subscriptions
use Plausible.Repo
@ -46,36 +19,7 @@ defmodule Plausible.Billing.Plans do
path
|> File.read!()
|> Jason.decode!(keys: :atoms!)
|> Enum.map(fn raw ->
team_member_limit =
case raw.team_member_limit do
number when is_integer(number) -> number
"unlimited" -> :unlimited
_any -> raise ArgumentError, "Failed to parse team member limit from plan JSON files"
end
features =
Plausible.Billing.Feature.list()
|> Enum.filter(fn module ->
to_string(module.name()) in raw.features
end)
if length(features) != length(raw.features),
do:
raise(
ArgumentError,
"Unrecognized feature(s) in #{inspect(raw.features)} (#{f}.json)"
)
volume = PlausibleWeb.StatsView.large_number_format(raw.monthly_pageview_limit)
raw
|> Map.put(:volume, volume)
|> Map.put(:kind, String.to_atom(raw.kind))
|> Map.put(:team_member_limit, team_member_limit)
|> Map.put(:features, features)
|> Plan.new()
end)
|> Enum.map(&Plan.build!(&1, f))
Module.put_attribute(__MODULE__, f, plans_list)
@ -94,26 +38,34 @@ defmodule Plausible.Billing.Plans do
def growth_plans_for(%User{} = user) do
user = Plausible.Users.with_subscription(user)
v4_available = FunWithFlags.enabled?(:business_tier, for: user)
owned_plan_id = user.subscription && user.subscription.paddle_plan_id
owned_plan = get_regular_plan(user.subscription)
cond do
find(owned_plan_id, @plans_v1) -> @plans_v1
find(owned_plan_id, @plans_v2) -> @plans_v2
find(owned_plan_id, @plans_v3) -> @plans_v3
find(owned_plan_id, plans_sandbox()) -> plans_sandbox()
Application.get_env(:plausible, :environment) == "dev" -> plans_sandbox()
Timex.before?(user.inserted_at, ~D[2022-01-01]) -> @plans_v2
v4_available -> Enum.filter(@plans_v4, &(&1.kind == :growth))
true -> @plans_v3
!owned_plan -> if v4_available, do: @plans_v4, else: @plans_v3
owned_plan.kind == :business -> @plans_v4
owned_plan.generation == 1 -> @plans_v1
owned_plan.generation == 2 -> @plans_v2
owned_plan.generation == 3 -> @plans_v3
owned_plan.generation == 4 -> @plans_v4
end
|> Enum.filter(&(&1.kind == :growth))
end
def business_plans() do
Enum.filter(@plans_v4, &(&1.kind == :business))
def business_plans_for(%User{} = user) do
user = Plausible.Users.with_subscription(user)
owned_plan = get_regular_plan(user.subscription)
cond do
Application.get_env(:plausible, :environment) == "dev" -> plans_sandbox()
owned_plan && owned_plan.generation < 4 -> @plans_v3
true -> @plans_v4
end
|> Enum.filter(&(&1.kind == :business))
end
def available_plans_with_prices(%User{} = user) do
(growth_plans_for(user) ++ business_plans())
(growth_plans_for(user) ++ business_plans_for(user))
|> with_prices()
|> Enum.group_by(& &1.kind)
end
@ -261,7 +213,7 @@ defmodule Plausible.Billing.Plans do
available_plans =
if business_tier?(user.subscription),
do: business_plans(),
do: business_plans_for(user),
else: growth_plans_for(user)
Enum.find(available_plans, &(usage_during_cycle < &1.monthly_pageview_limit))

View File

@ -64,6 +64,24 @@ defmodule PlausibleWeb.Live.ChoosePlan do
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 = growth_benefits(growth_plan_to_render)
business_benefits = business_benefits(business_plan_to_render, growth_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(business_benefits))
~H"""
<div class="bg-gray-100 dark:bg-gray-900 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-20">
@ -84,26 +102,20 @@ defmodule PlausibleWeb.Live.ChoosePlan do
<.plan_box
kind={:growth}
owned={@owned_plan && Map.get(@owned_plan, :kind) == :growth}
plan_to_render={
if @selected_growth_plan,
do: @selected_growth_plan,
else: List.last(@available_plans.growth)
}
plan_to_render={@growth_plan_to_render}
benefits={@growth_benefits}
available={!!@selected_growth_plan}
{assigns}
/>
<.plan_box
kind={:business}
owned={@owned_plan && Map.get(@owned_plan, :kind) == :business}
plan_to_render={
if @selected_business_plan,
do: @selected_business_plan,
else: List.last(@available_plans.business)
}
plan_to_render={@business_plan_to_render}
benefits={@business_benefits}
available={!!@selected_business_plan}
{assigns}
/>
<.enterprise_plan_box />
<.enterprise_plan_box benefits={@enterprise_benefits} />
</div>
<p class="mx-auto mt-8 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-gray-400">
<.usage usage={@usage} />
@ -275,30 +287,31 @@ defmodule PlausibleWeb.Live.ChoosePlan do
</.paddle_button>
<% end %>
</div>
<%= if @kind == :growth && @plan_to_render.generation < 4 do %>
<.growth_grandfathering_notice />
<% else %>
<ul
role="list"
class="mt-8 space-y-3 text-sm leading-6 text-gray-600 dark:text-gray-100 xl:mt-10"
>
<li class="flex gap-x-3">
<.check_icon class="text-indigo-600 dark:text-green-600" /> 5 products
</li>
<li class="flex gap-x-3">
<.check_icon class="text-indigo-600 dark:text-green-600" /> Up to 1,000 subscribers
</li>
<li class="flex gap-x-3">
<.check_icon class="text-indigo-600 dark:text-green-600" /> Basic analytics
</li>
<li class="flex gap-x-3">
<.check_icon class="text-indigo-600 dark:text-green-600" /> 48-hour support response time
</li>
<.plan_benefit :for={benefit <- @benefits}><%= benefit %></.plan_benefit>
</ul>
<% end %>
</div>
"""
end
defp growth_grandfathering_notice(assigns) do
~H"""
<ul 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.
</ul>
"""
end
def render_price_info(%{available: false} = assigns) do
~H"""
<p class="mt-6 flex items-baseline gap-x-1">
<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>
@ -356,6 +369,18 @@ defmodule PlausibleWeb.Live.ChoosePlan do
"""
end
slot :inner_block, required: true
attr :icon_color, :string, default: "indigo-600"
defp plan_benefit(assigns) do
~H"""
<li class="flex gap-x-3">
<.check_icon class={"text-#{@icon_color} dark:text-green-600"} />
<%= render_slot(@inner_block) %>
</li>
"""
end
defp contact_button(assigns) do
~H"""
<.link
@ -372,7 +397,10 @@ defmodule PlausibleWeb.Live.ChoosePlan do
defp enterprise_plan_box(assigns) do
~H"""
<div class="rounded-3xl px-6 sm:px-8 py-4 sm:py-6 bg-gray-900 shadow-xl dark:bg-gray-800 dark:ring-gray-600">
<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 dark:ring-gray-600"
>
<h3 class="text-lg font-semibold leading-8 text-white dark:text-gray-100">Enterprise</h3>
<p class="mt-6 flex items-baseline gap-x-1">
<span class="text-4xl font-bold tracking-tight text-white dark:text-gray-100">
@ -385,25 +413,9 @@ defmodule PlausibleWeb.Live.ChoosePlan do
role="list"
class="mt-8 space-y-3 text-sm leading-6 xl:mt-10 text-gray-300 dark:text-gray-100"
>
<li class="flex gap-x-3">
<.check_icon class="text-white dark:text-green-600" /> Unlimited products
</li>
<li class="flex gap-x-3">
<.check_icon class="text-white dark:text-green-600" /> Unlimited subscribers
</li>
<li class="flex gap-x-3">
<.check_icon class="text-white dark:text-green-600" /> Advanced analytics
</li>
<li class="flex gap-x-3">
<.check_icon class="text-white dark:text-green-600" />
1-hour, dedicated support response time
</li>
<li class="flex gap-x-3">
<.check_icon class="text-white dark:text-green-600" /> Marketing automations
</li>
<li class="flex gap-x-3">
<.check_icon class="text-white dark:text-green-600" /> Custom reporting tools
</li>
<.plan_benefit :for={benefit <- @benefits}>
<%= if is_binary(benefit), do: benefit, else: benefit.(assigns) %>
</.plan_benefit>
</ul>
</div>
"""
@ -471,7 +483,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
defp price_tag(%{plan_to_render: %Plan{monthly_cost: nil}} = assigns) do
~H"""
<span class="text-4xl font-bold tracking-tight text-gray-900">
<span class="text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100">
N/A
</span>
"""
@ -620,6 +632,77 @@ defmodule PlausibleWeb.Live.ChoosePlan do
PlausibleWeb.StatsView.large_number_format(volume)
end
defp growth_benefits(plan) do
[
team_member_limit_benefit(plan),
site_limit_benefit(plan),
"Intuitive, fast and privacy-friendly dashboard",
"Email/Slack reports",
"Google Analytics import"
]
|> Kernel.++(feature_benefits(plan))
end
defp business_benefits(plan, growth_benefits) do
[
"Everything in Growth",
team_member_limit_benefit(plan),
site_limit_benefit(plan)
]
|> Kernel.++(feature_benefits(plan))
|> Kernel.--(growth_benefits)
|> Kernel.++(["Priority support"])
end
defp enterprise_benefits(business_benefits) do
team_members =
if "Up to 10 team members" in business_benefits,
do: "10+ team members",
else: nil
[
"Everything in Business",
team_members,
"50+ sites",
&sites_api_benefit/1,
"Technical onboarding"
]
|> Enum.filter(& &1)
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.map(plan.features, fn feature_mod ->
case feature_mod.name() do
:goals -> "Goals and custom events"
: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
defp contact_link(), do: @contact_link
defp billing_faq_link(), do: @billing_faq_link

View File

@ -1,6 +1,7 @@
[
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":10000,
"monthly_product_id":"558018",
"yearly_product_id":"572810",
@ -10,6 +11,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":100000,
"monthly_product_id":"558745",
"yearly_product_id":"590752",
@ -19,6 +21,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":200000,
"monthly_product_id":"597485",
"yearly_product_id":"597486",
@ -28,6 +31,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":500000,
"monthly_product_id":"597487",
"yearly_product_id":"597488",
@ -37,6 +41,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":1000000,
"monthly_product_id":"597642",
"yearly_product_id":"597643",
@ -46,6 +51,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":2000000,
"monthly_product_id":"597309",
"yearly_product_id":"597310",
@ -55,6 +61,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":5000000,
"monthly_product_id":"597311",
"yearly_product_id":"597312",
@ -64,6 +71,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":10000000,
"monthly_product_id":"642352",
"yearly_product_id":"642354",
@ -73,6 +81,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":20000000,
"monthly_product_id":"642355",
"yearly_product_id":"642356",
@ -82,6 +91,7 @@
},
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":50000000,
"monthly_product_id":"650652",
"yearly_product_id":"650653",

View File

@ -1,6 +1,7 @@
[
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":10000,
"monthly_product_id":"654177",
"yearly_product_id":"653232",
@ -10,6 +11,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":100000,
"monthly_product_id":"654178",
"yearly_product_id":"653234",
@ -19,6 +21,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":200000,
"monthly_product_id":"653237",
"yearly_product_id":"653236",
@ -28,6 +31,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":500000,
"monthly_product_id":"653238",
"yearly_product_id":"653239",
@ -37,6 +41,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":1000000,
"monthly_product_id":"653240",
"yearly_product_id":"653242",
@ -46,6 +51,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":2000000,
"monthly_product_id":"653253",
"yearly_product_id":"653254",
@ -55,6 +61,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":5000000,
"monthly_product_id":"653255",
"yearly_product_id":"653256",
@ -64,6 +71,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":10000000,
"monthly_product_id":"654181",
"yearly_product_id":"653257",
@ -73,6 +81,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":20000000,
"monthly_product_id":"654182",
"yearly_product_id":"653258",
@ -82,6 +91,7 @@
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":50000000,
"monthly_product_id":"654183",
"yearly_product_id":"653259",

View File

@ -1,6 +1,7 @@
[
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":10000,
"monthly_product_id":"749342",
"yearly_product_id":"749343",
@ -10,6 +11,7 @@
},
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":100000,
"monthly_product_id":"749344",
"yearly_product_id":"749345",
@ -19,6 +21,7 @@
},
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":200000,
"monthly_product_id":"749346",
"yearly_product_id":"749347",
@ -28,6 +31,7 @@
},
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":500000,
"monthly_product_id":"749348",
"yearly_product_id":"749349",
@ -37,6 +41,7 @@
},
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":1000000,
"monthly_product_id":"749350",
"yearly_product_id":"749352",
@ -46,6 +51,7 @@
},
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":2000000,
"monthly_product_id":"749353",
"yearly_product_id":"749355",
@ -55,6 +61,7 @@
},
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":5000000,
"monthly_product_id":"749356",
"yearly_product_id":"749357",
@ -64,11 +71,92 @@
},
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":10000000,
"monthly_product_id":"749358",
"yearly_product_id":"749359",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":10000,
"monthly_product_id":"857481",
"yearly_product_id":"857482",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":100000,
"monthly_product_id":"857483",
"yearly_product_id":"857484",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":200000,
"monthly_product_id":"857486",
"yearly_product_id":"857487",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":500000,
"monthly_product_id":"857490",
"yearly_product_id":"857491",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":1000000,
"monthly_product_id":"857493",
"yearly_product_id":"857494",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":2000000,
"monthly_product_id":"857495",
"yearly_product_id":"857496",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":5000000,
"monthly_product_id":"857498",
"yearly_product_id":"857500",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":10000000,
"monthly_product_id":"857501",
"yearly_product_id":"857502",
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","revenue_goals","funnels","stats_api"]
}
]

View File

@ -1,146 +1,162 @@
[
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":10000,
"monthly_product_id":"857097",
"yearly_product_id":"857079",
"site_limit":10,
"team_member_limit":5,
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":100000,
"monthly_product_id":"857098",
"yearly_product_id":"857080",
"site_limit":10,
"team_member_limit":5,
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":200000,
"monthly_product_id":"857099",
"yearly_product_id":"857081",
"site_limit":10,
"team_member_limit":5,
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":500000,
"monthly_product_id":"857100",
"yearly_product_id":"857082",
"site_limit":10,
"team_member_limit":5,
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":1000000,
"monthly_product_id":"857101",
"yearly_product_id":"857083",
"site_limit":10,
"team_member_limit":5,
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":2000000,
"monthly_product_id":"857102",
"yearly_product_id":"857084",
"site_limit":10,
"team_member_limit":5,
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":5000000,
"monthly_product_id":"857103",
"yearly_product_id":"857085",
"site_limit":10,
"team_member_limit":5,
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":10000000,
"monthly_product_id":"857104",
"yearly_product_id":"857086",
"site_limit":10,
"team_member_limit":5,
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":10000,
"monthly_product_id":"857105",
"yearly_product_id":"857087",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":100000,
"monthly_product_id":"857106",
"yearly_product_id":"857088",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":200000,
"monthly_product_id":"857107",
"yearly_product_id":"857089",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":500000,
"monthly_product_id":"857108",
"yearly_product_id":"857090",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":1000000,
"monthly_product_id":"857109",
"yearly_product_id":"857091",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":2000000,
"monthly_product_id":"857110",
"yearly_product_id":"857092",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":5000000,
"monthly_product_id":"857111",
"yearly_product_id":"857093",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props","revenue_goals","funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":10000000,
"monthly_product_id":"857112",
"yearly_product_id":"857094",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props","revenue_goals","funnels","stats_api"]
}
]

View File

@ -1,146 +1,162 @@
[
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":10000,
"monthly_product_id":"63842",
"yearly_product_id":"63859",
"site_limit":10,
"team_member_limit":5,
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","stats_api"]
},
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":100000,
"monthly_product_id":"63843",
"yearly_product_id":"63860",
"site_limit":10,
"team_member_limit":5,
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","stats_api"]
},
{
"kind":"growth",
"generation":3,
"monthly_pageview_limit":200000,
"monthly_product_id":"63844",
"yearly_product_id":"63861",
"site_limit":10,
"team_member_limit":5,
"site_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props","stats_api"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":500000,
"monthly_product_id":"63845",
"yearly_product_id":"63862",
"site_limit":10,
"team_member_limit":5,
"features":["goals","props","stats_api"]
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":1000000,
"monthly_product_id":"63846",
"yearly_product_id":"63863",
"site_limit":10,
"team_member_limit":5,
"features":["goals","props","stats_api"]
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":2000000,
"monthly_product_id":"63847",
"yearly_product_id":"63864",
"site_limit":10,
"team_member_limit":5,
"features":["goals","props","stats_api"]
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":5000000,
"monthly_product_id":"63848",
"yearly_product_id":"63865",
"site_limit":10,
"team_member_limit":5,
"features":["goals","props","stats_api"]
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"growth",
"generation":4,
"monthly_pageview_limit":10000000,
"monthly_product_id":"63849",
"yearly_product_id":"63866",
"site_limit":10,
"team_member_limit":5,
"features":["goals","props","stats_api"]
"team_member_limit":3,
"features":["goals"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":10000,
"monthly_product_id":"63850",
"yearly_product_id":"63867",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props", "revenue_goals", "funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":100000,
"monthly_product_id":"63851",
"yearly_product_id":"63868",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props", "revenue_goals", "funnels","stats_api"]
},
{
"kind":"business",
"generation":3,
"monthly_pageview_limit":200000,
"monthly_product_id":"63852",
"yearly_product_id":"63869",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":"unlimited",
"features":["goals","props", "revenue_goals", "funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":500000,
"monthly_product_id":"63853",
"yearly_product_id":"63870",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props", "revenue_goals", "funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":1000000,
"monthly_product_id":"63854",
"yearly_product_id":"63871",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props", "revenue_goals", "funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":2000000,
"monthly_product_id":"63855",
"yearly_product_id":"63872",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props", "revenue_goals", "funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":5000000,
"monthly_product_id":"63856",
"yearly_product_id":"63873",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props", "revenue_goals", "funnels","stats_api"]
},
{
"kind":"business",
"generation":4,
"monthly_pageview_limit":10000000,
"monthly_product_id":"63857",
"yearly_product_id":"63874",
"site_limit":50,
"team_member_limit":50,
"team_member_limit":10,
"features":["goals","props", "revenue_goals", "funnels","stats_api"]
}
]

View File

@ -1,6 +1,7 @@
[
{
"kind":"growth",
"generation":1,
"monthly_pageview_limit":150000000,
"yearly_product_id":"648089",
"monthly_product_id":null,

View File

@ -1,6 +1,7 @@
[
{
"kind":"growth",
"generation":2,
"monthly_pageview_limit":10000000,
"monthly_product_id":"655350",
"yearly_product_id":null,

View File

@ -5,30 +5,22 @@ defmodule Plausible.Billing.PlansTest do
@v1_plan_id "558018"
@v2_plan_id "654177"
@v4_plan_id "857097"
@v3_business_plan_id "857481"
@v4_business_plan_id "857105"
describe "getting subscription plans for user" do
test "growth_plans_for/1 shows v1 pricing for users who are already on v1 pricing" do
test "growth_plans_for/1 returns v1 plans for users who are already on v1 pricing" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v1_plan_id
end
test "growth_plans_for/1 shows v2 pricing for users who are already on v2 pricing" do
test "growth_plans_for/1 returns v2 plans for users who are already on v2 pricing" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v2_plan_id
end
test "growth_plans_for/1 shows v2 pricing for users who signed up in 2021" do
user = insert(:user, inserted_at: ~N[2021-12-31 00:00:00])
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v2_plan_id
end
test "growth_plans_for/1 shows v4 pricing for everyone else" do
test "growth_plans_for/1 returns v4 plans for everyone else" do
user = insert(:user)
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v4_plan_id
end
@ -41,11 +33,28 @@ defmodule Plausible.Billing.PlansTest do
end)
end
test "business_plans/0 returns only v4 business plans" do
Plans.business_plans()
|> Enum.each(fn plan ->
assert plan.kind == :business
end)
test "growth_plans_for/1 returns the latest generation of growth plans for a user with a business subscription" do
user =
insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id))
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v4_plan_id
end
test "business_plans_for/1 returns v3 business plans for a legacy subscriber" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
business_plans = Plans.business_plans_for(user)
assert Enum.all?(business_plans, &(&1.kind == :business))
assert List.first(business_plans).monthly_product_id == @v3_business_plan_id
end
test "business_plans_for/1 returns v4 business plans for everyone else" do
user = insert(:user)
business_plans = Plans.business_plans_for(user)
assert Enum.all?(business_plans, &(&1.kind == :business))
assert List.first(business_plans).monthly_product_id == @v4_business_plan_id
end
test "available_plans_with_prices/1" do
@ -58,7 +67,7 @@ defmodule Plausible.Billing.PlansTest do
end)
assert Enum.find(business_plans, fn plan ->
(%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v4_business_plan_id
(%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v3_business_plan_id
end)
end
@ -177,6 +186,14 @@ defmodule Plausible.Billing.PlansTest do
"749355",
"749357",
"749359",
"857482",
"857484",
"857487",
"857491",
"857494",
"857496",
"857500",
"857502",
"857079",
"857080",
"857081",

View File

@ -6,6 +6,7 @@ defmodule Plausible.Billing.QuotaTest do
@v1_plan_id "558018"
@v2_plan_id "654177"
@v3_plan_id "749342"
@v3_business_plan_id "857481"
describe "site_limit/1" do
test "returns 50 when user is on an old plan" do
@ -356,8 +357,15 @@ defmodule Plausible.Billing.QuotaTest do
user_on_business = insert(:user, subscription: build(:business_subscription))
assert 5 == Quota.team_member_limit(user_on_growth)
assert 50 == Quota.team_member_limit(user_on_business)
assert 3 == Quota.team_member_limit(user_on_growth)
assert 10 == Quota.team_member_limit(user_on_business)
end
test "returns unlimited when user is on a v3 business plan" do
user =
insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id))
assert :unlimited == Quota.team_member_limit(user)
end
end

View File

@ -8,6 +8,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
@v1_10k_yearly_plan_id "572810"
@v4_growth_200k_yearly_plan_id "857081"
@v4_business_5m_monthly_plan_id "857111"
@v3_business_10k_monthly_plan_id "857481"
@monthly_interval_button ~s/label[phx-click="set_interval"][phx-value-interval="monthly"]/
@yearly_interval_button ~s/label[phx-click="set_interval"][phx-value-interval="yearly"]/
@ -27,6 +28,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
@business_current_label "#{@business_plan_box} #current-label"
@business_checkout_button "#business-checkout"
@enterprise_plan_box "#enterprise-plan-box"
describe "for a user with no subscription" do
setup [:create_user, :log_in]
@ -41,6 +44,41 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert doc =~ "+ VAT if applicable"
end
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
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 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 business_box =~ "Everything in Growth"
assert business_box =~ "Up to 10 team members"
assert business_box =~ "Up to 50 sites"
assert business_box =~ "Stats API"
assert business_box =~ "Custom Properties"
assert business_box =~ "Funnels"
assert business_box =~ "Ecommerce revenue attribution"
assert business_box =~ "Priority support"
refute business_box =~ "Goals and custom events"
assert enterprise_box =~ "Everything in Business"
assert enterprise_box =~ "10+ team members"
assert enterprise_box =~ "50+ sites"
assert enterprise_box =~ "Sites API access for"
assert enterprise_box =~ "Technical onboarding"
assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~
"https://plausible.io/white-label-web-analytics"
end
test "default billing interval is monthly, and can switch to yearly", %{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
@ -107,16 +145,16 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = lv |> element(@slider_input) |> render_change(%{slider: 8})
assert text_of_element(doc, @growth_plan_box) =~ "Custom"
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_plan_box) =~ "Custom"
assert text_of_element(doc, "#business-custom-price") =~ "Custom"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
doc = lv |> element(@slider_input) |> render_change(%{slider: 7})
refute text_of_element(doc, @growth_plan_box) =~ "Custom"
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_plan_box) =~ "Custom"
refute text_of_element(doc, "#business-custom-price") =~ "Custom"
refute text_of_element(doc, @business_plan_box) =~ "Contact us"
end
@ -175,6 +213,41 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
refute doc =~ "What happens if I go over my page views limit?"
end
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
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 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 business_box =~ "Everything in Growth"
assert business_box =~ "Up to 10 team members"
assert business_box =~ "Up to 50 sites"
assert business_box =~ "Stats API"
assert business_box =~ "Custom Properties"
assert business_box =~ "Funnels"
assert business_box =~ "Ecommerce revenue attribution"
assert business_box =~ "Priority support"
refute business_box =~ "Goals and custom events"
assert enterprise_box =~ "Everything in Business"
assert enterprise_box =~ "10+ team members"
assert enterprise_box =~ "50+ sites"
assert enterprise_box =~ "Sites API access for"
assert enterprise_box =~ "Technical onboarding"
assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~
"https://plausible.io/white-label-web-analytics"
end
test "displays usage", %{conn: conn, user: user} do
site = insert(:site, members: [user])
@ -302,6 +375,51 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
end
end
describe "for a user with a v3 business (unlimited team members) subscription plan" do
setup [:create_user, :log_in]
setup %{user: user} = context do
create_subscription_for(user, paddle_plan_id: @v3_business_10k_monthly_plan_id)
{:ok, context}
end
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
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 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 business_box =~ "Everything in Growth"
assert business_box =~ "Unlimited team members"
assert business_box =~ "Up to 50 sites"
assert business_box =~ "Stats API"
assert business_box =~ "Custom Properties"
assert business_box =~ "Funnels"
assert business_box =~ "Ecommerce revenue attribution"
assert business_box =~ "Priority support"
refute business_box =~ "Goals and custom events"
assert enterprise_box =~ "Everything in Business"
assert enterprise_box =~ "50+ sites"
assert enterprise_box =~ "Sites API access for"
assert enterprise_box =~ "Technical onboarding"
refute enterprise_box =~ "team members"
assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~
"https://plausible.io/white-label-web-analytics"
end
end
describe "for a user with a past_due subscription" do
setup [:create_user, :log_in, :create_past_due_subscription]
@ -391,10 +509,13 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
describe "for a grandfathered user" do
setup [:create_user, :log_in]
test "on a v1 plan, Growth tiers are available at 20M, 50M, 50M+, but Business tiers are not",
%{conn: conn, user: user} do
setup %{user: user} = context do
create_subscription_for(user, paddle_plan_id: @v1_10k_yearly_plan_id)
{:ok, context}
end
test "on a v1 plan, Growth tiers are available at 20M, 50M, 50M+, but Business tiers are not",
%{conn: conn} do
{:ok, lv, _doc} = get_liveview(conn)
doc = lv |> element(@slider_input) |> render_change(%{slider: 8})
@ -419,6 +540,42 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
refute text_of_element(doc, @business_plan_box) =~ "Contact us"
refute text_of_element(doc, @growth_plan_box) =~ "Contact us"
end
test "displays grandfathering notice in the Growth box instead of benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
growth_box = text_of_element(doc, @growth_plan_box)
assert growth_box =~ "Your subscription has been grandfathered"
refute growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
end
test "displays business and enterprise plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
assert business_box =~ "Everything in Growth"
assert business_box =~ "Funnels"
assert business_box =~ "Ecommerce revenue attribution"
assert business_box =~ "Priority support"
refute business_box =~ "Goals and custom events"
refute business_box =~ "Unlimited team members"
refute business_box =~ "Up to 50 sites"
refute business_box =~ "Stats API"
refute business_box =~ "Custom Properties"
assert enterprise_box =~ "Everything in Business"
assert enterprise_box =~ "50+ sites"
assert enterprise_box =~ "Sites API access for"
assert enterprise_box =~ "Technical onboarding"
assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~
"https://plausible.io/white-label-web-analytics"
refute enterprise_box =~ "10+ team members"
refute enterprise_box =~ "Unlimited team members"
end
end
describe "for a free_10k subscription" do