382 lines
15 KiB
Elixir
382 lines
15 KiB
Elixir
defmodule PlausibleWeb.Live.ChoosePlan do
|
|
@moduledoc """
|
|
LiveView for upgrading to a plan, or changing an existing plan.
|
|
"""
|
|
use PlausibleWeb, :live_view
|
|
|
|
require Plausible.Billing.Subscription.Status
|
|
|
|
alias PlausibleWeb.Components.Billing.{PlanBox, Notice, PageviewSlider}
|
|
alias Plausible.Billing.{Plans, Quota, PlanBenefits}
|
|
alias PlausibleWeb.Router.Helpers, as: Routes
|
|
|
|
@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)
|
|
end)
|
|
|> assign_new(:recommended_tier, fn %{
|
|
usage: usage,
|
|
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_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)
|
|
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 || :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,
|
|
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)
|
|
|
|
starter_plan_to_render =
|
|
assigns.selected_starter_plan || List.last(assigns.available_plans.starter)
|
|
|
|
starter_benefits =
|
|
if starter_plan_to_render do
|
|
PlanBenefits.for_starter(starter_plan_to_render)
|
|
else
|
|
[]
|
|
end
|
|
|
|
growth_benefits =
|
|
PlanBenefits.for_growth(growth_plan_to_render, starter_benefits)
|
|
|
|
business_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)
|
|
|
|
~H"""
|
|
<div class="pt-1 pb-12 sm:pb-16 text-gray-900 dark:text-gray-100">
|
|
<div class={[
|
|
"mx-auto px-6 lg:px-8",
|
|
if(is_nil(@starter_plan_to_render), do: "max-w-5xl", else: "max-w-7xl")
|
|
]}>
|
|
<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="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-semibold 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="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-semibold"
|
|
>
|
|
← 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-4 lg:max-w-none lg:mx-0",
|
|
if(is_nil(@starter_plan_to_render), do: "lg:grid-cols-3", else: "lg:grid-cols-4")
|
|
]}>
|
|
<PlanBox.standard
|
|
:if={@growth_plan_to_render.generation == 5}
|
|
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}
|
|
recommended={@recommended_tier == :growth}
|
|
plan_to_render={@growth_plan_to_render}
|
|
benefits={@growth_benefits}
|
|
available={!!@selected_growth_plan}
|
|
{assigns}
|
|
/>
|
|
<PlanBox.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}
|
|
/>
|
|
<PlanBox.enterprise
|
|
benefits={@enterprise_benefits}
|
|
recommended={@recommended_tier == :custom}
|
|
/>
|
|
</div>
|
|
<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>
|
|
<PlausibleWeb.Components.Billing.paddle_script />
|
|
"""
|
|
end
|
|
|
|
defp render_usage(assigns) do
|
|
~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
|
|
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_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)
|
|
)}
|
|
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-sm 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 help_links(assigns) do
|
|
~H"""
|
|
<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
|
|
|
|
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
|