diff --git a/lib/plausible_web/components/billing/plan_box.ex b/lib/plausible_web/components/billing/plan_box.ex index 37e21b407645..69514703f3aa 100644 --- a/lib/plausible_web/components/billing/plan_box.ex +++ b/lib/plausible_web/components/billing/plan_box.ex @@ -39,7 +39,9 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
<.render_price_info available={@available} {assigns} /> <%= if @available do %> - <.checkout id={"#{@kind}-checkout"} {assigns} /> +
+ <.checkout id={"#{@kind}-checkout"} {assigns} /> +
<% else %> <.contact_button class="bg-indigo-600 hover:bg-indigo-500 text-white" /> <% end %> @@ -164,33 +166,36 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do ]) subscription_deleted = Subscription.Status.deleted?(subscription) + usage_within_limits = usage_within_plan_limits?(assigns) - {checkout_disabled, disabled_message} = + {checkout_disabled, disabled_message, limits} = cond do not assigns.eligible_for_upgrade? -> - {true, nil} + {true, nil, nil} change_plan_link_text == "Currently on this plan" && not subscription_deleted -> - {true, nil} + {true, nil, nil} - assigns.available && !usage_within_plan_limits?(assigns) -> - {true, "Your usage exceeds this plan"} + usage_within_limits != :ok -> + {true, "Your usage exceeds this plan", limits(usage_within_limits)} billing_details_expired -> - {true, "Please update your billing details first"} + {true, "Please update your billing details first", nil} true -> - {false, nil} + {false, nil, nil} end features_to_lose = assigns.usage.features -- assigns.plan_to_render.features assigns = assigns + |> assign(:display_tooltip_product_id, assigns[:display_tooltip_product_id]) |> 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(:disabled_message_limits, limits) |> assign(:confirm_message, losing_features_message(features_to_lose)) ~H""" @@ -201,13 +206,75 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do Upgrade <% end %> - """ end - defp usage_within_plan_limits?(%{usage: usage, user: user, plan_to_render: plan}) do + attr :show_tooltip?, :boolean, default: false + attr :paddle_product_id, :string, required: true + attr :limits, :list, default: nil + attr :disabled_message, :string, required: true + + def disabled_with_tooltip(assigns) do + opacity = + if assigns.show_tooltip? do + "opacity-90" + else + "opacity-0 group-hover:opacity-90" + end + + assigns = assign(assigns, opacity: opacity) + + ~H""" +
+ + Your usage exceeds the following limit(s):

+ <%= for limit <- @limits do %> + <%= Phoenix.Naming.humanize(limit) %> + <% end %> +
+ + + <%= @disabled_message %> + + +
+ """ + end + + defp usage_within_plan_limits?(%{available: false}) do + {:error, :plan_unavailable} + end + + defp usage_within_plan_limits?(%{ + available: true, + usage: usage, + user: user, + plan_to_render: plan + }) do # At this point, the user is *not guaranteed* to have a `trial_expiry_date`, # because in the past we've let users upgrade without that constraint, as # well as transfer sites to those accounts. to these accounts we won't be @@ -232,9 +299,15 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do [] end - Quota.ensure_within_plan_limits(usage, plan, limit_checking_opts) == :ok + Quota.ensure_within_plan_limits(usage, plan, limit_checking_opts) end + defp limits({:error, {:over_plan_limits, limits}}) do + limits + end + + defp limits(_), do: nil + 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 diff --git a/lib/plausible_web/live/choose_plan.ex b/lib/plausible_web/live/choose_plan.ex index 39d2866afead..671a3e32a715 100644 --- a/lib/plausible_web/live/choose_plan.ex +++ b/lib/plausible_web/live/choose_plan.ex @@ -189,6 +189,14 @@ defmodule PlausibleWeb.Live.ChoosePlan do )} end + def handle_event("show-tooltip", %{"paddle-product-id" => product_id}, socket) do + {:noreply, assign(socket, display_tooltip_product_id: product_id)} + end + + def handle_event("hide-tooltip", _, socket) do + {:noreply, assign(socket, display_tooltip_product_id: nil)} + end + defp default_selected_volume(%Plan{monthly_pageview_limit: limit}, _, _), do: limit defp default_selected_volume(_, last_30_days_usage, available_volumes) do diff --git a/test/plausible_web/live/choose_plan_test.exs b/test/plausible_web/live/choose_plan_test.exs index a463cdbdcab1..7df389c1ac70 100644 --- a/test/plausible_web/live/choose_plan_test.exs +++ b/test/plausible_web/live/choose_plan_test.exs @@ -19,6 +19,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do @slider_value "#slider-value" @growth_plan_box "#growth-plan-box" + @growth_plan_tooltip "#growth-plan-box .tooltip-wrapper span" + @growth_plan_tooltip_link ~s/#growth-plan-box .tooltip-wrapper a[phx-click="show-tooltip"]/ @growth_price_tag_amount "#growth-price-tag-amount" @growth_price_tag_interval "#growth-price-tag-interval" @growth_highlight_pill "#{@growth_plan_box} #highlight-pill" @@ -243,6 +245,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none" refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none" + refute element_exists?(doc, @growth_plan_tooltip) generate_usage_for(site, 1) @@ -251,6 +254,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none" assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none" + + assert text_of_element(doc, @growth_plan_tooltip) == + "Your usage exceeds the following limit(s): Monthly pageview limit" end test "allows upgrade to a 10k plan with a pageview allowance margin of 0.3 when trial ended 10 days ago", @@ -497,6 +503,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan" assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none" + + assert text_of_element(doc, @growth_plan_tooltip) == + "Your usage exceeds the following limit(s): Team member limit" end test "checkout is disabled when sites usage exceeds rendered plan limit", %{ @@ -509,6 +518,55 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan" assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none" + + assert text_of_element(doc, @growth_plan_tooltip) == + "Your usage exceeds the following limit(s): Site limit" + end + + test "clicking the tooltip changes makes it sticky", %{ + conn: conn, + user: user + } do + insert(:site, + memberships: [ + build(:site_membership, user: user, role: :owner), + build(:site_membership, user: build(:user)), + build(:site_membership, user: build(:user)), + build(:site_membership, user: build(:user)), + build(:site_membership, user: build(:user)) + ] + ) + + {:ok, lv, doc} = get_liveview(conn) + + assert class_of_element(doc, @growth_plan_tooltip) =~ "opacity-0 group-hover:opacity-90" + + doc = element(lv, @growth_plan_tooltip_link) |> render_click() + + assert class_of_element(doc, @growth_plan_tooltip) =~ "opacity-90" + refute class_of_element(doc, @growth_plan_tooltip) =~ "opacity-0" + end + + test "when more than one limit is exceeded, the tooltip enumerates them", %{ + conn: conn, + user: user + } do + for _ <- 1..11, do: insert(:site, members: [user]) + + insert(:site, + memberships: [ + build(:site_membership, user: user, role: :owner), + build(:site_membership, user: build(:user)), + build(:site_membership, user: build(:user)), + build(:site_membership, user: build(:user)), + build(:site_membership, user: build(:user)) + ] + ) + + {:ok, _lv, doc} = get_liveview(conn) + + assert text_of_element(doc, @growth_plan_tooltip) =~ "Team member limit" + assert text_of_element(doc, @growth_plan_tooltip) =~ "Site limit" end test "checkout is not disabled when pageview usage exceeded but next upgrade allowed by override",