Omit Subscription and Invoices menu on CE (#5856)

* Update mua

* Fix issue with unexpected menu items

* Update changelog
This commit is contained in:
Artur Pata 2025-11-03 11:23:10 +02:00 committed by GitHub
parent a204c89066
commit 457c483416
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 215 additions and 51 deletions

View File

@ -35,6 +35,8 @@ All notable changes to this project will be documented in this file.
when they occurred.
- Fixed realtime and hourly graphs of visits overcounting
- When reporting only `visitors` and `visits` per hour, count visits in each hour they were active in.
- Remove Subscription and Invoices menu from CE
- Fix email sending error "Mua.SMTPError" 503 Bad sequence of commands
## v3.0.0 - 2025-04-11

View File

@ -59,7 +59,7 @@ defmodule PlausibleWeb.Components.Layout do
def settings_sidebar(assigns) do
~H"""
<div class="flex flex-col gap-0.5 -ml-2">
<div class="flex flex-col gap-0.5 -ml-2" data-testid="settings-sidebar">
<.settings_top_tab
:for={%{key: key, value: value, icon: icon} = opts <- @options}
selected_fn={@selected_fn}

View File

@ -429,7 +429,7 @@ defmodule PlausibleWeb.Live.Components.Form do
assigns = assign(assigns, :options, flatten_options(options))
~H"""
<.form for={@conn} class="lg:hidden py-4">
<.form for={@conn} class="lg:hidden py-4" data-testid="mobile-nav-dropdown">
<.input
value={
@options

View File

@ -93,10 +93,10 @@ defmodule PlausibleWeb.LayoutView do
[
%{key: "Preferences", value: "preferences", icon: :cog_6_tooth},
%{key: "Security", value: "security", icon: :lock_closed},
if(not Teams.setup?(current_team),
if(ee?() and not Teams.setup?(current_team),
do: %{key: "Subscription", value: "billing/subscription", icon: :circle_stack}
),
if(not Teams.setup?(current_team) and subscription?,
if(ee?() and not Teams.setup?(current_team) and subscription?,
do: %{key: "Invoices", value: "billing/invoices", icon: :banknotes}
),
if(not Teams.setup?(current_team),
@ -115,10 +115,10 @@ defmodule PlausibleWeb.LayoutView do
"Team",
[
%{key: "General", value: "team/general", icon: :adjustments_horizontal},
if(current_team_role in [:owner, :billing],
if(ee?() and current_team_role in [:owner, :billing],
do: %{key: "Subscription", value: "billing/subscription", icon: :circle_stack}
),
if(current_team_role in [:owner, :billing] and subscription?,
if(ee?() and current_team_role in [:owner, :billing] and subscription?,
do: %{key: "Invoices", value: "billing/invoices", icon: :banknotes}
),
if(current_team_role in [:owner, :billing, :admin, :editor],

View File

@ -92,7 +92,7 @@
"mjml": {:hex, :mjml, "3.1.0", "549e985bc03be1af563c62a34c8e62bdb8d0baaa6b31af705a5bdf67e20f22b7", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7.0", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "987674d296b14b628e5e5d2d8b910e6501cdfafa0239527d8b633880dc595344"},
"mjml_eex": {:hex, :mjml_eex, "0.11.0", "f0845730f4caccddea7c98ab5ad1485831446b7c09896fa5ed54b3fa0c431e72", [:mix], [{:erlexec, "~> 2.0", [hex: :erlexec, repo: "hexpm", optional: true]}, {:mjml, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :mjml, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0c60732fe766336ec504a94cad4ebf30405f05fa8920a544ff0ef936252438ac"},
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
"mua": {:hex, :mua, "0.2.4", "a9172ab0a1ac8732cf2699d739ceac3febcb9b4ffc540260ad2e32c0b6632af9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "e7e4dacd5ad65f13e3542772e74a159c00bd2d5579e729e9bb72d2c73a266fb7"},
"mua": {:hex, :mua, "0.2.5", "e99aa9646964a0109a2efcc8e684c6f8d90c60fb0191f52e1784cea296584daf", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "0e2b18024d0db8943a68e84fb5e2253d3225c8f61d8387cbfc581d66e34d8493"},
"nanoid": {:hex, :nanoid, "2.1.0", "d192a5bf1d774258bc49762b480fca0e3128178fa6d35a464af2a738526607fd", [:mix], [], "hexpm", "ebc7a342d02d213534a7f93a091d569b9fea7f26fcd3a638dc655060fc1f76ac"},
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},

View File

@ -1345,10 +1345,41 @@ defmodule PlausibleWeb.SettingsControllerTest do
end
end
@menu_items [
preferences: {"Preferences", "/settings/preferences"},
security: {"Security", "/settings/security"},
subscription: {"Subscription", "/settings/billing/subscription"},
invoices: {"Invoices", "/settings/billing/invoices"},
api_keys: {"API keys", "/settings/api-keys"},
danger_zone: {"Danger zone", "/settings/danger-zone"},
team_general: {"General", "/settings/team/general"},
sso: {"Single Sign-On", "/settings/sso/info"},
team_danger_zone: {"Danger zone", "/settings/team/delete"}
]
on_ee do
describe "Account Settings - SSO user" do
setup [:create_user, :create_site, :create_team, :setup_sso, :provision_sso_user, :log_in]
test "shows only expected menu items", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :preferences))
assert html = html_response(conn, 200)
expected_account_menu = [:preferences, :security, :subscription, :api_keys]
html
|> refute_unexpected_menu_items([
:invoices,
:team_general,
:sso,
:team_danger_zone,
:danger_zone
])
|> Floki.parse_document!()
|> assert_sidebar_menu(expected_account_menu)
|> assert_mobile_menu(expected_account_menu)
end
test "does not allow to update name in preferences", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :preferences))
assert html = html_response(conn, 200)
@ -1375,67 +1406,69 @@ defmodule PlausibleWeb.SettingsControllerTest do
assert html = html_response(conn, 200)
assert text_of_element(html, "button[disabled]") =~ "Disable 2FA"
end
test "does not show account danger zone", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :preferences))
assert html = html_response(conn, 200)
refute html =~ "/settings/danger-zone"
end
end
end
describe "Team Settings" do
setup [:create_user, :log_in]
test "does not render team settings, when no team assigned", %{conn: conn} do
test "when no team is assigned & the user doesn't have a subscription, limited account menu is present",
%{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :preferences))
html = html_response(conn, 200)
refute html =~ "Team"
expected_account_menu =
if(ee?(),
do: [:preferences, :security, :subscription, :api_keys, :danger_zone],
else: [:preferences, :security, :api_keys, :danger_zone]
)
html
|> refute_unexpected_menu_items(
if(ee?(),
do: [:invoices, :team_general, :sso],
else: [:subscription, :invoices, :team_general, :sso]
)
)
|> Floki.parse_document!()
|> assert_sidebar_menu(expected_account_menu)
|> assert_mobile_menu(expected_account_menu)
end
test "does not render invoices when no subscription present (no team assigned)", %{conn: conn} do
conn = get(conn, Routes.settings_path(conn, :preferences))
html = html_response(conn, 200)
refute html =~ Routes.settings_path(conn, :invoices)
end
test "does render invoices when subscription present (no team assigned)", %{
conn: conn,
user: user
} do
test "when no team is assigned & the user has a subscription, the account menu contains invoices",
%{
conn: conn,
user: user
} do
subscribe_to_growth_plan(user)
conn = get(conn, Routes.settings_path(conn, :preferences))
html = html_response(conn, 200)
assert html =~ Routes.settings_path(conn, :invoices)
end
test "does not render invoices when no subscription (team set up)", %{
conn: conn,
user: user
} do
{:ok, team} = Plausible.Teams.get_or_create(user)
team = Plausible.Teams.complete_setup(team)
conn = set_current_team(conn, team)
conn = get(conn, Routes.settings_path(conn, :preferences))
html = html_response(conn, 200)
refute html =~ Routes.settings_path(conn, :invoices)
end
test "does render invoices when subscription present (team assigned)", %{
conn: conn,
user: user
} do
subscribe_to_growth_plan(user)
{:ok, team} = Plausible.Teams.get_or_create(user)
team = Plausible.Teams.complete_setup(team)
conn = set_current_team(conn, team)
conn = get(conn, Routes.settings_path(conn, :preferences))
html = html_response(conn, 200)
assert html =~ Routes.settings_path(conn, :invoices)
expected_account_menu =
if(ee?(),
do: [:preferences, :security, :subscription, :invoices, :api_keys, :danger_zone],
else: [:preferences, :security, :api_keys, :danger_zone]
)
html
|> refute_unexpected_menu_items(
if(ee?(),
do: [:team_general, :sso],
else: [:subscription, :invoices, :team_general, :sso]
)
)
|> Floki.parse_document!()
|> assert_sidebar_menu(expected_account_menu)
|> assert_mobile_menu(expected_account_menu)
end
test "renders team settings, when team assigned and set up", %{conn: conn, user: user} do
test "when team is set up & there's no subscription, renders limited account & team menu",
%{
conn: conn,
user: user
} do
{:ok, team} = Plausible.Teams.get_or_create(user)
team = Plausible.Teams.complete_setup(team)
conn = set_current_team(conn, team)
@ -1443,6 +1476,66 @@ defmodule PlausibleWeb.SettingsControllerTest do
html = html_response(conn, 200)
assert html =~ ~r/Team.*#{Regex.escape(team.name)}/s
assert html =~ team.name
expected_account_menu = [
:preferences,
:security,
:danger_zone
]
expected_team_menu =
if(ee?(),
do: [:team_general, :subscription, :api_keys, :sso, :team_danger_zone],
else: [:team_general, :api_keys, :team_danger_zone]
)
html
|> refute_unexpected_menu_items(
if(ee?(), do: [:invoices], else: [:subscription, :invoices])
)
|> Floki.parse_document!()
|> assert_sidebar_menu(expected_account_menu, expected_team_menu)
|> assert_mobile_menu(expected_account_menu, expected_team_menu)
end
test "when team is set up, and there's a subscription, renders account & team menu with invoices",
%{
conn: conn,
user: user
} do
subscribe_to_growth_plan(user)
{:ok, team} = Plausible.Teams.get_or_create(user)
team = Plausible.Teams.complete_setup(team)
conn = set_current_team(conn, team)
conn = get(conn, Routes.settings_path(conn, :preferences))
html = html_response(conn, 200)
assert html =~ ~r/Team.*#{Regex.escape(team.name)}/s
assert html =~ team.name
expected_account_menu = [
:preferences,
:security,
:danger_zone
]
expected_team_menu =
if(ee?(),
do: [
:team_general,
:subscription,
:invoices,
:api_keys,
:sso,
:team_danger_zone
],
else: [:team_general, :api_keys, :team_danger_zone]
)
html
|> Floki.parse_document!()
|> assert_sidebar_menu(expected_account_menu, expected_team_menu)
|> assert_mobile_menu(expected_account_menu, expected_team_menu)
end
test "does not render team settings, when team not set up", %{conn: conn, user: user} do
@ -1645,4 +1738,73 @@ defmodule PlausibleWeb.SettingsControllerTest do
)
)
end
defp assert_sidebar_menu(document, ordered_account_menu_keys, ordered_team_menu_keys \\ []) do
ordered_menu_keys = Enum.concat(ordered_account_menu_keys, ordered_team_menu_keys)
assert get_expected_menu(ordered_menu_keys) == get_sidebar_menu_items(document)
document
end
defp assert_mobile_menu(
document,
ordered_account_menu_keys,
ordered_team_menu_keys \\ []
) do
expected_account_items =
ordered_account_menu_keys
|> get_expected_menu()
|> Enum.map(fn {text, "/settings/" <> path_fragment} ->
{"Account: #{text}", path_fragment}
end)
expected_team_items =
ordered_team_menu_keys
|> get_expected_menu()
|> Enum.map(fn {text, "/settings/" <> path_fragment} ->
{"Team: #{text}", path_fragment}
end)
assert Enum.concat(
expected_account_items,
expected_team_items
) ==
get_mobile_menu_options(document)
document
end
defp get_expected_menu(ordered_menu_keys) do
ordered_menu_keys
|> Keyword.new(&{&1, nil})
|> Keyword.intersect(@menu_items)
|> Keyword.values()
end
defp refute_unexpected_menu_items(html, unexpected_menu_keys) do
refuted_menu_items = @menu_items |> Keyword.take(unexpected_menu_keys) |> Keyword.values()
for {text, link} <- refuted_menu_items do
refute html =~ text
refute html =~ link
end
html
end
defp get_mobile_menu_options(document) do
Floki.find(document, "[data-testid='mobile-nav-dropdown'] option")
|> Enum.map(&parse_option/1)
end
defp parse_option(option),
do: {Floki.text(option), Floki.attribute(option, "value") |> List.first()}
defp get_sidebar_menu_items(document) do
Floki.find(document, "[data-testid='settings-sidebar'] a")
|> Enum.map(&parse_link/1)
end
defp parse_link(link),
do: {Floki.text(link) |> String.trim(), Floki.attribute(link, "href") |> List.first()}
end