defmodule PlausibleWeb.Components.Billing.PlanBox do @moduledoc false use PlausibleWeb, :component require Plausible.Billing.Subscription.Status alias PlausibleWeb.Components.Billing.{PlanBenefits} 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 assigns.owned && assigns.recommended -> "Current" assigns.recommended -> "Recommended" true -> nil end assigns = assigns |> assign(:highlight, highlight) |> assign(:price_container_class, @plan_box_price_container_class) ~H"""

{String.capitalize(to_string(@kind))}

<.highlight_pill :if={@highlight} text={@highlight} />
<.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 %>
""" end def enterprise(assigns) do assigns = assign(assigns, :price_container_class, @plan_box_price_container_class) ~H"""

Enterprise

Recommended
Custom
<.contact_button class="" />
""" end defp highlight_pill(assigns) do ~H"""

{@text}

""" end defp render_price_info(%{available: false} = assigns) do ~H"""

Custom

""" end defp render_price_info(assigns) do ~H""" <.price_tag kind={@kind} selected_interval={@selected_interval} plan_to_render={@plan_to_render} />
+ VAT
""" 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"""

{@monthly_cost} /month

""" 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"""
{@yearly_cost} /year
{@monthly_cost} {@monthly_cost_with_discount}
/month
""" 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 %> Upgrade <% end %>
<.tooltip testid="plan-tooltip"> <:tooltip_content> Your usage exceeds the following limit(s):

{Phoenix.Naming.humanize(limit)}

{@disabled_message}
""" 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 -> [skip_pageview_limit_check?: 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 in [:growth, :business] && to_kind == :starter -> "Downgrade to Starter" from_kind == :business && to_kind == :growth -> "Downgrade to Growth" 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 -> "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""" """ 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 """ end end