diff --git a/lib/plausible_web/components/flow_progress.ex b/lib/plausible_web/components/flow_progress.ex index 705a00a2c725..d07d4154078f 100644 --- a/lib/plausible_web/components/flow_progress.ex +++ b/lib/plausible_web/components/flow_progress.ex @@ -1,44 +1,39 @@ defmodule PlausibleWeb.Components.FlowProgress do @moduledoc """ - Dotted progress indicator shown during the registration flow. - One small dot per step in `PlausibleWeb.Flows.steps/1`, with completed - and current steps highlighted. + Dotted progress indicator shown in the onboarding layout. + One small dot per step, with completed and current steps highlighted. """ use Phoenix.Component - attr :flow, :string, required: true, values: PlausibleWeb.Flows.valid_keys() - attr :current_step, :string, required: true, values: PlausibleWeb.Flows.valid_values() + attr :steps, :list, required: true + attr :current_step, :string, required: true def render(assigns) do - steps = PlausibleWeb.Flows.steps(assigns.flow) - current_step_idx = Enum.find_index(steps, &(&1 == assigns.current_step)) + current_step_idx = Enum.find_index(assigns.steps, &(&1 == assigns.current_step)) - assigns = - assign(assigns, - steps: steps, - current_step_idx: current_step_idx - ) + assigns = assign(assigns, :current_step_idx, current_step_idx) ~H""" -
+
"h-2 w-5 rounded-full bg-indigo-600 dark:bg-gray-100" - idx < @current_step_idx -> "size-2 rounded-full bg-indigo-600 dark:bg-gray-100" - true -> "size-2 rounded-full bg-gray-300 dark:bg-gray-600" - end - } + class={dot_class(dot_state(idx, @current_step_idx))} aria-current={idx == @current_step_idx && "step"} aria-label={step} />
""" end + + @doc """ + The classnames for a progress dot in the given state. Exposed so tests can + assert on rendered dots without duplicating the Tailwind classnames. + """ + def dot_class(:completed), do: "size-2 rounded-full bg-indigo-600 dark:bg-gray-100" + def dot_class(:current), do: "h-2 w-5 rounded-full bg-indigo-600 dark:bg-gray-100" + def dot_class(:upcoming), do: "size-2 rounded-full bg-gray-300 dark:bg-gray-600" + + defp dot_state(idx, current_idx) when idx < current_idx, do: :completed + defp dot_state(idx, current_idx) when idx == current_idx, do: :current + defp dot_state(_idx, _current_idx), do: :upcoming end diff --git a/lib/plausible_web/components/layouts.ex b/lib/plausible_web/components/layouts.ex index 6e3ddca889a0..548373adcff7 100644 --- a/lib/plausible_web/components/layouts.ex +++ b/lib/plausible_web/components/layouts.ex @@ -136,4 +136,33 @@ defmodule PlausibleWeb.Layouts do """ end + + attr :current_user, :any, default: nil + attr :current_step, :string, required: true + attr :flash, :map, default: %{} + slot :inner_block, required: true + + def onboarding(assigns) do + ~H""" + <.app + footer?={false} + global_notices?={false} + trial_badge?={false} + current_user={@current_user} + flash={@flash} + > +
+
+ {render_slot(@inner_block)} +
+
+ +
+
+ + """ + end end diff --git a/lib/plausible_web/components/site/new_site_form.ex b/lib/plausible_web/components/site/new_site_form.ex new file mode 100644 index 000000000000..32f02545dba7 --- /dev/null +++ b/lib/plausible_web/components/site/new_site_form.ex @@ -0,0 +1,77 @@ +defmodule PlausibleWeb.Components.Site.NewSiteForm do + @moduledoc false + + use PlausibleWeb, :component + + attr :changeset, Ecto.Changeset, required: true + attr :form_submit_url, :string, required: true + attr :site_limit_exceeded?, :boolean, required: true + attr :site_limit, :integer, required: true + attr :current_user, :any, required: true + attr :current_team, :any, required: true + attr :back_button_text, :string, required: true + attr :back_button_href, :string, required: true + + def new_site_form(assigns) do + ~H""" + <.heading_and_subtitle heading="Add a website" subtitle="Start measuring traffic on a new site." /> +
+ <.form :let={f} class="flex flex-col gap-y-8" for={@changeset} action={@form_submit_url}> + + + <.input + help_text="Just the naked domain or subdomain without 'www', 'https' etc." + type="text" + placeholder="example.com" + field={f[:domain]} + label="Domain" + disabled={@site_limit_exceeded?} + mt?={false} + autofocus="autofocus" + /> + + + + +
+ <.button_link theme="ghost" href={@back_button_href} mt?={false}> + {@back_button_text} + + <.button + disabled={@site_limit_exceeded?} + type="submit" + mt?={false} + class="disabled:cursor-not-allowed" + > + Add site + +
+ +
+ """ + end + + def heading_and_subtitle(assigns) do + ~H""" +
+

{@heading}

+

+ {@subtitle} +

+
+ """ + end +end diff --git a/lib/plausible_web/controllers/helpers.ex b/lib/plausible_web/controllers/helpers.ex index 8d65e5ca6cd7..f04a08889564 100644 --- a/lib/plausible_web/controllers/helpers.ex +++ b/lib/plausible_web/controllers/helpers.ex @@ -23,22 +23,6 @@ defmodule PlausibleWeb.ControllerHelpers do defp error_layout, do: Application.get_env(:plausible, PlausibleWeb.Endpoint)[:render_errors][:layout] - @onboarding_layout_assigns [ - layout: {PlausibleWeb.LayoutView, :onboarding}, - hide_footer?: true, - disable_global_notices?: true, - bg_class: "bg-white dark:bg-gray-950" - ] - - def render_onboarding_page(conn, template, extra_assigns \\ []) do - assigns = - @onboarding_layout_assigns - |> Keyword.put(:hide_trial_badge?, conn.params["flow"] == PlausibleWeb.Flows.register()) - |> Keyword.merge(extra_assigns) - - render(conn, template, assigns) - end - def debug_metadata(conn) do %{ request_method: conn.method, diff --git a/lib/plausible_web/controllers/site_controller.ex b/lib/plausible_web/controllers/site_controller.ex index 1ee302d5e335..9aac75d9bcee 100644 --- a/lib/plausible_web/controllers/site_controller.ex +++ b/lib/plausible_web/controllers/site_controller.ex @@ -72,11 +72,9 @@ defmodule PlausibleWeb.SiteController do defaults = [ changeset: Plausible.Site.changeset(%Plausible.Site{}), site_limit_exceeded?: false, - flow: flow, form_submit_url: "/sites?flow=#{flow}", - current_step: "Add site info", - heading: "Add a website", - subtitle: "Start measuring traffic on a new site." + bg_class: "bg-white dark:bg-gray-950", + legacy_layout?: false ] assigns = @@ -86,7 +84,11 @@ defmodule PlausibleWeb.SiteController do Plausible.Teams.Billing.site_limit(conn.assigns.current_team) end) - render_onboarding_page(conn, "new.html", assigns) + if flow == PlausibleWeb.Flows.register() do + render(conn, "onboarding_new_site.html", assigns) + else + render(conn, "provisioning_new_site.html", assigns) + end end def settings(conn, %{"domain" => domain}) do diff --git a/lib/plausible_web/flows.ex b/lib/plausible_web/flows.ex index 706dc2ac5956..81c427bbdf96 100644 --- a/lib/plausible_web/flows.ex +++ b/lib/plausible_web/flows.ex @@ -1,58 +1,62 @@ defmodule PlausibleWeb.Flows do @moduledoc """ - Static compile-time definitions for user progress flows. - See `PlausibleWeb.Components.FlowProgress` for rendering capabilities. + Named identifiers and documentation for the onboarding/installation flows. """ - @flows %{ - review: [ - "Install Plausible" - ], - domain_change: [ - "Set up new domain", - "Install Plausible" - ], - register: [ - "Add site info", - "Install Plausible" - ], - invitation: [ - "Register", - "Activate account" - ], - provisioning: [ - "Add site info", - "Install Plausible" - ] - } - - @valid_values @flows - |> Enum.flat_map(fn {_, steps} -> steps end) - |> Enum.uniq() - - @valid_keys @flows - |> Map.keys() - |> Enum.map(&to_string/1) - - @spec steps(binary() | atom()) :: list(binary()) - def steps(flow) when flow in @valid_keys do - steps(String.to_existing_atom(flow)) - end - - def steps(flow) when is_atom(flow) do - Map.get(@flows, flow, []) - end - - def steps(_), do: [] - - @spec valid_values() :: list(binary()) - def valid_values(), do: @valid_values - - @spec valid_values() :: list(binary()) - def valid_keys(), do: @valid_keys - - for {flow, _} <- @flows do - @spec unquote(flow)() :: binary() - def unquote(flow)(), do: unquote(to_string(flow)) - end + @doc """ + The flow of creating a new account from scratch: + + * Register form + * Activate account (email confirmation) + * ...optional onboarding steps (see `onboarding_steps/0`) + * End up either on the dashboard (with verification kicked off automatically) + or the /sites page, depending on whether a site was setup or not. + """ + def register, do: "register" + + @doc """ + The flow of an already logged in user adding another site to their team: + + * Add site info + * Installation screen + * End up either on the dashboard (with verification kicked off automatically) + or the /sites page, depending on whether installation was skipped or not + """ + def provisioning, do: "provisioning" + + @doc """ + The flow of an existing user reviewing/re-checking their already-installed + tracking script (triggered from site settings): + + * Installation screen, pre-filled with the earlier installation method + * End up on the dashboard (with verification kicked off automatically) + """ + def review, do: "review" + + @doc """ + The flow of an existing site domain getting changed (triggered from + site settings): + + * Domain name change form + * Domain change success info page (possible further actions required) + * Back to site settings + """ + def domain_change, do: "domain_change" + + @doc """ + The flow of accepting a team/site invitation as a new user: + + * Register form + * Activate account (email confirmation) + * Land on the /sites page + """ + def invitation, do: "invitation" + + @doc """ + The steps shown in the onboarding flow's progress indicator. + """ + def onboarding_steps, do: [add_site_step(), installation_step()] + + def add_site_step, do: "Add site info" + def installation_step, do: "Install Plausible" end diff --git a/lib/plausible_web/live/installation.ex b/lib/plausible_web/live/installation.ex index f4ef6cf634cf..ed7565986ed4 100644 --- a/lib/plausible_web/live/installation.ex +++ b/lib/plausible_web/live/installation.ex @@ -6,7 +6,7 @@ defmodule PlausibleWeb.Live.Installation do use Plausible use PlausibleWeb, :live_view - alias PlausibleWeb.Flows + alias PlausibleWeb.{Flows, Layouts} alias Phoenix.LiveView.AsyncResult alias PlausibleWeb.Live.Installation.Icons alias PlausibleWeb.Live.Installation.Instructions @@ -93,7 +93,6 @@ defmodule PlausibleWeb.Live.Installation do site: site, flow: flow, return_to: params["return_to"], - current_step: "Install Plausible", heading: heading, subtitle: subtitle )} @@ -117,7 +116,11 @@ defmodule PlausibleWeb.Live.Installation do assigns = assign(assigns, :submit_button_text, @submit_button_text) ~H""" -
+ <.onboarding_or_app_layout {assigns}> +
<.async_result :let={recommended_installation_type} assign={@recommended_installation_type}> <:loading> @@ -233,7 +236,7 @@ defmodule PlausibleWeb.Live.Installation do
-
+ """ end @@ -323,6 +326,34 @@ defmodule PlausibleWeb.Live.Installation do end end + defp onboarding_or_app_layout(assigns) do + if assigns.flow == Flows.register() do + ~H""" + + {render_slot(@inner_block)} + + """ + else + ~H""" + + {render_slot(@inner_block)} + + """ + end + end + attr :flow, :string, required: true attr :return_to, :string, default: nil attr :domain, :string, required: true diff --git a/lib/plausible_web/live/onboarding_layout_context.ex b/lib/plausible_web/live/onboarding_layout_context.ex deleted file mode 100644 index 2ac4a5563778..000000000000 --- a/lib/plausible_web/live/onboarding_layout_context.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule PlausibleWeb.Live.OnboardingLayoutContext do - @moduledoc false - - import Phoenix.Component - - alias PlausibleWeb.Flows - - def on_mount(_arg, params, _session, socket) do - socket = - assign(socket, - hide_trial_badge?: params["flow"] == Flows.register(), - hide_footer?: true, - disable_global_notices?: true, - bg_class: "bg-white dark:bg-gray-950" - ) - - {:cont, socket, layout: {PlausibleWeb.LayoutView, :onboarding}} - end -end diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index fd2c69f891fc..c6d51695298f 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -627,16 +627,16 @@ defmodule PlausibleWeb.Router do scope alias: Live, assigns: %{connect_live_socket: true} do pipe_through [:app_layout, PlausibleWeb.RequireAccountPlug] - live_session :onboarding, on_mount: PlausibleWeb.Live.OnboardingLayoutContext do - scope assigns: %{ - dogfood_page_path: "/:website/installation" - } do - live "/:domain/installation", - Installation, - :installation, - as: :site, - container: {:div, class: "flex-1 flex flex-col"} - end + scope assigns: %{ + dogfood_page_path: "/:website/installation", + bg_class: "bg-white dark:bg-gray-950", + legacy_layout?: false + } do + live "/:domain/installation", + Installation, + :installation, + as: :site, + container: {:div, class: "h-full"} end scope assigns: %{ diff --git a/lib/plausible_web/templates/layout/onboarding.html.heex b/lib/plausible_web/templates/layout/onboarding.html.heex deleted file mode 100644 index 967c20c5a1be..000000000000 --- a/lib/plausible_web/templates/layout/onboarding.html.heex +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

{@heading}

-

- {@subtitle} -

-
- {@inner_content} -
-
- -
-
diff --git a/lib/plausible_web/templates/site/new.html.heex b/lib/plausible_web/templates/site/new.html.heex deleted file mode 100644 index 1f4ad3805300..000000000000 --- a/lib/plausible_web/templates/site/new.html.heex +++ /dev/null @@ -1,46 +0,0 @@ -
- <.form :let={f} class="flex flex-col gap-y-8" for={@changeset} action={@form_submit_url}> - - - <.input - help_text="Just the naked domain or subdomain without 'www', 'https' etc." - type="text" - placeholder="example.com" - field={f[:domain]} - label="Domain" - disabled={@site_limit_exceeded?} - mt?={false} - autofocus="autofocus" - /> - - - - -
- <.button_link theme="ghost" href={Routes.site_path(@conn, :index)} mt?={false}> - {if @flow == PlausibleWeb.Flows.provisioning(), do: "Back to sites", else: "Skip"} - - <.button - disabled={@site_limit_exceeded?} - type="submit" - mt?={false} - class="disabled:cursor-not-allowed" - > - Add site - -
- -
diff --git a/lib/plausible_web/templates/site/onboarding_new_site.html.heex b/lib/plausible_web/templates/site/onboarding_new_site.html.heex new file mode 100644 index 000000000000..330d8465dd14 --- /dev/null +++ b/lib/plausible_web/templates/site/onboarding_new_site.html.heex @@ -0,0 +1,16 @@ + + + diff --git a/lib/plausible_web/templates/site/provisioning_new_site.html.heex b/lib/plausible_web/templates/site/provisioning_new_site.html.heex new file mode 100644 index 000000000000..e9fdd707bd62 --- /dev/null +++ b/lib/plausible_web/templates/site/provisioning_new_site.html.heex @@ -0,0 +1,20 @@ + + + diff --git a/lib/plausible_web/views/site_view.ex b/lib/plausible_web/views/site_view.ex index 23b49bd16372..ca2fea45024a 100644 --- a/lib/plausible_web/views/site_view.ex +++ b/lib/plausible_web/views/site_view.ex @@ -2,6 +2,9 @@ defmodule PlausibleWeb.SiteView do use PlausibleWeb, :view use Plausible + alias PlausibleWeb.Layouts + alias PlausibleWeb.Components.Site.NewSiteForm + def plausible_url do PlausibleWeb.Endpoint.url() end diff --git a/test/plausible_web/components/flow_progress_test.exs b/test/plausible_web/components/flow_progress_test.exs index 63bf85b5310f..6eddee3c49e7 100644 --- a/test/plausible_web/components/flow_progress_test.exs +++ b/test/plausible_web/components/flow_progress_test.exs @@ -5,80 +5,24 @@ defmodule PlausibleWeb.Components.FlowProgressTest do alias PlausibleWeb.Components.FlowProgress - test "no flow or unknown flow renders nothing" do - rendered = - render_component(&FlowProgress.render/1, - flow: nil, - current_step: "unhandled" - ) - - assert rendered == "" - - rendered = - render_component(&FlowProgress.render/1, - flow: "unhandled", - current_step: "unhandled" - ) - - assert rendered == "" - end + @steps ["A", "B", "C", "D"] - test "register" do + test "marks steps before, at, and after the current step correctly" do rendered = render_component(&FlowProgress.render/1, - flow: PlausibleWeb.Flows.register(), - current_step: "Add site info" + steps: @steps, + current_step: "B" ) - assert_dot_labels(rendered, ["Add site info", "Install Plausible"]) + assert_dot_labels(rendered, @steps) + assert_current_step(rendered, "B") - assert_current_step(rendered, "Add site info") - end - - test "invitation" do - rendered = - render_component(&FlowProgress.render/1, - flow: PlausibleWeb.Flows.invitation(), - current_step: "Register" - ) - - assert_dot_labels(rendered, ["Register", "Activate account"]) - assert_current_step(rendered, "Register") - end - - test "provisioning" do - rendered = - render_component(&FlowProgress.render/1, - flow: PlausibleWeb.Flows.provisioning(), - current_step: "Add site info" - ) - - assert_dot_labels(rendered, ["Add site info", "Install Plausible"]) - - assert_current_step(rendered, "Add site info") - end - - test "review" do - rendered = - render_component(&FlowProgress.render/1, - flow: PlausibleWeb.Flows.review(), - current_step: "Install Plausible" - ) - - assert_dot_labels(rendered, ["Install Plausible"]) - assert_current_step(rendered, "Install Plausible") - end - - test "domain_change" do - rendered = - render_component(&FlowProgress.render/1, - flow: PlausibleWeb.Flows.domain_change(), - current_step: "Set up new domain" - ) - - assert_dot_labels(rendered, ["Set up new domain", "Install Plausible"]) - - assert_current_step(rendered, "Set up new domain") + assert_dot_classes(rendered, [ + {"A", FlowProgress.dot_class(:completed)}, + {"B", FlowProgress.dot_class(:current)}, + {"C", FlowProgress.dot_class(:upcoming)}, + {"D", FlowProgress.dot_class(:upcoming)} + ]) end defp assert_dot_labels(rendered, expected_labels) do @@ -100,4 +44,14 @@ defmodule PlausibleWeb.Components.FlowProgressTest do assert Enum.count(current) == 1 assert LazyHTML.attribute(current, "aria-label") == [expected_label] end + + defp assert_dot_classes(rendered, expected_label_class_pairs) do + html = LazyHTML.from_fragment(rendered) + + Enum.each(expected_label_class_pairs, fn {label, expected_class} -> + assert html + |> LazyHTML.query(~s(#flow-progress [aria-label="#{label}"])) + |> LazyHTML.attribute("class") == [expected_class] + end) + end end diff --git a/test/plausible_web/controllers/site_controller_test.exs b/test/plausible_web/controllers/site_controller_test.exs index 3dbb58ce583a..6afff4a88fa8 100644 --- a/test/plausible_web/controllers/site_controller_test.exs +++ b/test/plausible_web/controllers/site_controller_test.exs @@ -25,16 +25,24 @@ defmodule PlausibleWeb.SiteControllerTest do assert html_response(conn, 200) =~ "Add a website" end - test "shows onboarding steps regardless of sites provisioned", %{conn: conn1, user: user} do + test "default flow is 'register', shows onboarding steps regardless of sites provisioned", %{ + conn: conn1, + user: user + } do conn = get(conn1, "/sites/new") - assert html_response(conn, 200) =~ "Add site info" + assert html_response(conn, 200) =~ ~s(id="flow-progress") new_site(owner: user, domain: "test-site.com") conn = get(conn1, "/sites/new") - assert html_response(conn, 200) =~ "Add site info" + assert html_response(conn, 200) =~ ~s(id="flow-progress") + end + + test "does not show onboarding steps when ?flow=provisioning", %{conn: conn} do + conn = get(conn, "/sites/new?flow=provisioning") + refute html_response(conn, 200) =~ ~s(id="flow-progress") end test "does not display limit notice when user is on an enterprise plan", %{