Omit Subscription and Invoices menu on CE (#5856)
* Update mua * Fix issue with unexpected menu items * Update changelog
This commit is contained in:
parent
a204c89066
commit
457c483416
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
2
mix.lock
2
mix.lock
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue