analytics/lib/plausible_web/live/choose_plan.ex

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