defmodule PlausibleWeb.Components.Billing do @moduledoc false use PlausibleWeb, :component use Plausible require Plausible.Billing import PlausibleWeb.Components.Icons alias Plausible.Billing.{Plan, Plans, EnterprisePlan} attr :site, Plausible.Site, required: false, default: nil attr :current_user, Plausible.Auth.User, required: true attr :current_team, :any, required: true attr :locked?, :boolean, required: true attr :link_class, :string, default: "" slot :inner_block, required: true def feature_gate(assigns) do ~H"""
{render_slot(@inner_block)}

Upgrade to unlock

To access this feature, <.upgrade_call_to_action current_user={@current_user} current_team={@current_team} link_class={@link_class} />
""" end attr :usage, :map, required: true attr :limit, :any, required: true attr :total_pageview_usage_domain, :string, default: nil def render_monthly_pageview_usage(%{usage: usage} = assigns) when is_map_key(usage, :last_30_days) do ~H""" <.monthly_pageview_usage_breakdown usage={@usage.last_30_days} limit={@limit} period={:last_30_days} expanded={Enum.count(@usage.last_30_days.per_site) <= 1} total_pageview_usage_domain={@total_pageview_usage_domain} /> """ end def render_monthly_pageview_usage(assigns) do exceeded = Plausible.Billing.Quota.exceeded_cycles(assigns.usage, assigns.limit, with_margin: false) show_all = :last_cycle in exceeded or :current_cycle in exceeded assigns = assign(assigns, :show_all, show_all) ~H"""
<.monthly_pageview_usage_breakdown usage={@usage.current_cycle} limit={@limit} period={:current_cycle} expanded={not @show_all and Enum.count(@usage.current_cycle.per_site) <= 1} total_pageview_usage_domain={@total_pageview_usage_domain} /> <%= if @show_all do %> <.monthly_pageview_usage_breakdown usage={@usage.last_cycle} limit={@limit} period={:last_cycle} expanded={false} total_pageview_usage_domain={@total_pageview_usage_domain} /> <.monthly_pageview_usage_breakdown usage={@usage.penultimate_cycle} limit={@limit} period={:penultimate_cycle} expanded={false} total_pageview_usage_domain={@total_pageview_usage_domain} /> <% end %>
""" end attr(:usage, :map, required: true) attr(:limit, :any, required: true) attr(:period, :atom, required: true) attr(:expanded, :boolean, required: true) attr(:total_pageview_usage_domain, :string, default: nil) defp monthly_pageview_usage_breakdown(assigns) do assigns = assign( assigns, :total_link, dashboard_url( assigns.total_pageview_usage_domain, assigns.usage.date_range ) ) ~H"""

{PlausibleWeb.TextHelpers.format_date_range(@usage.date_range)} {cycle_label(@period)}

<.usage_progress_bar :if={@limit != :unlimited} id={"total_pageviews_#{@period}"} usage={@usage.total} limit={@limit} />
<.pageview_usage_row id={"pageviews_#{@period}"} label={if Enum.empty?(@usage.per_site), do: "Pageviews", else: "Total pageviews"} value={@usage.pageviews} /> <.pageview_usage_row id={"custom_events_#{@period}"} label={if Enum.empty?(@usage.per_site), do: "Custom events", else: "Total custom events"} value={@usage.custom_events} />

0} class="border-gray-200 dark:border-gray-700" />
{site.domain} <.tooltip centered?={true}> <:tooltip_content>View billing period in dashboard <.link href={dashboard_url(site.domain, @usage.date_range)} class="shrink-0 text-indigo-500 hover:text-indigo-600" > <.external_link_icon class="ml-0.5 size-3.5 [&_path]:stroke-2" /> {PlausibleWeb.TextHelpers.number_format(site.total)}
<.pageview_usage_row label="Pageviews" value={site.pageviews} /> <.pageview_usage_row label="Custom events" value={site.custom_events} />
""" end attr :id, :string, default: nil attr :label, :string, required: true attr :value, :integer, required: true defp pageview_usage_row(assigns) do ~H"""
{@label} {PlausibleWeb.TextHelpers.number_format(@value)}
""" end defp dashboard_url(nil, _date_range), do: nil defp dashboard_url(domain, date_range) do base = Routes.stats_path(PlausibleWeb.Endpoint, :stats, domain, []) base <> "?period=custom&from=#{Date.to_iso8601(date_range.first)}&to=#{Date.to_iso8601(date_range.last)}" end defp cycle_label(:current_cycle), do: "(current cycle)" defp cycle_label(:last_30_days), do: "(last 30 days)" @doc """ Renders a color-coded progress bar based on usage percentage. Color scheme: - 0-90%: Green (healthy usage) - 91-99%: Gradient from green through yellow to orange (approaching limit) - 100%: Gradient from green through orange to red (at limit) """ attr(:usage, :integer, required: true) attr(:limit, :any, required: true) attr(:rest, :global) def usage_progress_bar(assigns) do percentage = calculate_percentage(assigns.usage, assigns.limit) assigns = assigns |> assign(:percentage, percentage) |> assign(:color_class, progress_bar_color_from_percentage(percentage, assigns.limit)) ~H"""
""" end defp calculate_percentage(_usage, :unlimited), do: 0 defp calculate_percentage(_usage, 0), do: 0 defp calculate_percentage(usage, limit) when is_number(limit) do percentage = usage / limit * 100 min(percentage, 100.0) |> Float.round(1) end defp progress_bar_color_from_percentage(_percentage, :unlimited), do: "bg-green-500 dark:bg-green-600" defp progress_bar_color_from_percentage(_percentage, 0), do: "bg-gray-200 dark:bg-gray-700" defp progress_bar_color_from_percentage(percentage, _limit) when is_number(percentage) do cond do percentage >= 100.0 -> "bg-gradient-to-r from-green-500 via-orange-500 via-[80%] to-red-500 dark:from-green-600 dark:via-orange-600 dark:to-red-600" percentage >= 91 -> "bg-gradient-to-r from-green-500 via-yellow-500 via-[80%] to-orange-500 dark:from-green-600 dark:via-yellow-600 dark:to-orange-600" true -> "bg-green-500 dark:bg-green-600" end end def present_enterprise_plan(assigns) do ~H""" """ end defp present_limit(enterprise_plan, key) do enterprise_plan |> Map.fetch!(key) |> PlausibleWeb.StatsView.large_number_format() end attr :id, :string, required: true attr :paddle_product_id, :string, required: true attr :checkout_disabled, :boolean, default: false attr :user, :map, required: true attr :team, :map, default: nil attr :confirm_message, :any, default: nil slot :inner_block, required: true def paddle_button(assigns) do js_action_expr = start_paddle_checkout_expr(assigns.paddle_product_id, assigns.team, assigns.user) confirmed = if assigns.confirm_message, do: "confirm(\"#{assigns.confirm_message}\")", else: "true" assigns = assigns |> assign(:confirmed, confirmed) |> assign(:js_action_expr, js_action_expr) ~H""" """ end if Mix.env() == :dev do def start_paddle_checkout_expr(paddle_product_id, _team, _user) do "window.location = '#{Routes.dev_subscription_path(PlausibleWeb.Endpoint, :create_form, paddle_product_id)}'" end def paddle_script(assigns), do: ~H"" else def start_paddle_checkout_expr(paddle_product_id, team, user) do passthrough = if team do "ee:#{ee?()};user:#{user.id};team:#{team.id}" else "ee:#{ee?()};user:#{user.id}" end paddle_checkout_params = Jason.encode!(%{ product: paddle_product_id, email: user.email, disableLogout: true, passthrough: passthrough, success: Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), theme: "none" }) "Paddle.Checkout.open(#{paddle_checkout_params})" end def paddle_script(assigns) do ~H""" """ end end attr :link_class, :string, default: "" attr :current_team, :any, required: true attr :current_user, :atom, required: true def upgrade_call_to_action(assigns) do user = assigns.current_user site = assigns[:site] team = Plausible.Teams.with_subscription(assigns.current_team) current_role = cond do not is_nil(site) -> case Plausible.Teams.Memberships.site_role(site, user) do {:ok, {_, site_role}} -> site_role _ -> nil end not is_nil(team) -> case Plausible.Teams.Memberships.team_role(team, user) do {:ok, team_role} -> team_role _ -> nil end true -> nil end upgrade_assistance_required? = case Plans.get_subscription_plan(team && team.subscription) do %Plan{kind: :business} -> true %EnterprisePlan{} -> true _ -> false end cond do not is_nil(current_role) and current_role not in Plausible.Billing.allowed_roles() -> ~H"ask your team owner to upgrade their subscription." upgrade_assistance_required? -> ~H""" contact <.styled_link href="mailto:hello@plausible.io" class={"font-medium " <> @link_class}> hello@plausible.io to upgrade your subscription. """ true -> ~H""" <.styled_link class={"inline-block font-medium " <> @link_class} href={Routes.billing_path(PlausibleWeb.Endpoint, :choose_plan)} > upgrade your subscription. """ end end end