defmodule PlausibleWeb.Live.Installation do @moduledoc """ User assistance module around Plausible installation instructions/onboarding """ use Plausible use PlausibleWeb, :live_view alias PlausibleWeb.Flows alias Phoenix.LiveView.AsyncResult alias PlausibleWeb.Live.Installation.Icons alias PlausibleWeb.Live.Installation.Instructions on_ee do alias Plausible.InstallationSupport.{Detection, Result} end def mount( %{"domain" => domain} = params, _session, socket ) do site = Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, roles: [ :owner, :admin, :editor, :super_admin, :viewer ] ) flow = params["flow"] || Flows.provisioning() socket = on_ee do if connected?(socket) do assign_async( socket, [ :recommended_installation_type, :installation_type, :tracker_script_configuration_form, :v1_detected ], fn -> initialize_installation_data(flow, site, params) end ) else assign_loading_states(socket) end else # On Community Edition, there's no v1 detection, nor pre-installation # site scan - we just default the pre-selected tab to "manual". # Although it's functionally unnecessary, we stick to using `%AsyncResult{}` # for these assigns to minimize branching out the CE code and maintain only # a single `render` function. {:ok, installation_data} = initialize_installation_data(flow, site, params) assign(socket, recommended_installation_type: %AsyncResult{ result: installation_data.recommended_installation_type, ok?: true }, installation_type: %AsyncResult{ result: installation_data.installation_type, ok?: true }, tracker_script_configuration_form: %AsyncResult{ result: installation_data.tracker_script_configuration_form, ok?: true }, v1_detected: %AsyncResult{ result: installation_data.v1_detected, ok?: true } ) end {:ok, assign(socket, site: site, site_created?: params["site_created"] == "true", flow: flow )} end def handle_params(params, _url, socket) do socket = if connected?(socket) && socket.assigns.recommended_installation_type.result && params["type"] in PlausibleWeb.Tracker.supported_installation_types() do assign(socket, installation_type: %AsyncResult{result: params["type"]} ) else socket end {:noreply, socket} end def render(assigns) do ~H"""
<.focus_box> <.async_result :let={recommended_installation_type} assign={@recommended_installation_type}> <:loading>
{if(@flow == Flows.review(), do: "Scanning your site to detect how Plausible is integrated...", else: "Determining the simplest integration path for your website..." )}
<.spinner class="w-6 h-6" />
<.tab patch={"?type=manual&flow=#{@flow}"} selected={@installation_type.result == "manual"} > Script <.tab patch={"?type=wordpress&flow=#{@flow}"} selected={@installation_type.result == "wordpress"} > WordPress <%= on_ee do %> <.tab patch={"?type=gtm&flow=#{@flow}"} selected={@installation_type.result == "gtm"}> Tag Manager <% end %> <.tab patch={"?type=npm&flow=#{@flow}"} selected={@installation_type.result == "npm"}> NPM
<%= on_ee do %> <.outdated_script_notice :if={@v1_detected.result == true} recommended_installation_type={@recommended_installation_type} installation_type={@installation_type} /> <% end %> <.form for={@tracker_script_configuration_form.result} phx-submit="submit" class="mt-4"> <.input type="hidden" field={@tracker_script_configuration_form.result[:installation_type]} value={@installation_type.result} /> <%= on_ee do %> <% end %> <.button type="submit" class={ "w-full mt-8 " <> install_method_event_classes( @installation_type.result, recommended_installation_type ) } > {verify_cta(@installation_type.result)} <:footer :if={ce?() and @installation_type.result == "manual"}> <.focus_list> <:item> Still using the legacy snippet with the data-domain attribute? See <.styled_link href="https://plausible.io/docs/script-update-guide"> migration guide
""" end defp verify_cta("manual"), do: "Verify Script installation" defp verify_cta("wordpress"), do: "Verify WordPress installation" defp verify_cta("gtm"), do: "Verify Tag Manager installation" defp verify_cta("npm"), do: "Verify NPM installation" defp install_method_event_classes(installation_type, recommended) do method = installation_method_label(installation_type) match = if installation_type == recommended, do: "true", else: "false" Enum.join( [ "plausible-event-name=Site+installation+method", "plausible-event-method=#{method}", "plausible-event-recommended_match=#{match}" ], " " ) end defp installation_method_label("manual"), do: "script" defp installation_method_label(other), do: other on_ee do defp detect_recommended_installation_type(flow, site) do with {:ok, detection_result} <- Detection.Checks.run_with_rate_limit(nil, site.domain, detect_v1?: flow == Flows.review(), report_to: nil, slowdown: 0, async?: false ), %Result{ok?: true, data: data} <- Detection.Checks.interpret_diagnostics(detection_result) do {data.suggested_technology, data.v1_detected} else _ -> {PlausibleWeb.Tracker.fallback_installation_type(), false} end end else defp detect_recommended_installation_type(_flow, _site) do {PlausibleWeb.Tracker.fallback_installation_type(), false} end end on_ee do defp outdated_script_notice(assigns) do ~H"""
<.notice class="mt-4" theme={:yellow}> Your website is running an outdated version of the tracking script. Please <.styled_link new_tab href="https://plausible.io/docs/script-update-guide"> update your tracking script before continuing
<.notice class="mt-4" theme={:yellow}> Your website might be using an outdated version of our Google Tag Manager template. If so, <.styled_link new_tab href="https://plausible.io/docs/script-update-guide#gtm"> update your Google Tag Manager template before continuing
""" end defp assign_loading_states(socket) do assign(socket, recommended_installation_type: AsyncResult.loading(), v1_detected: AsyncResult.loading(), installation_type: AsyncResult.loading(), tracker_script_configuration_form: AsyncResult.loading() ) end end attr :selected, :boolean, default: false attr :patch, :string, required: true slot :inner_block, required: true defp tab(assigns) do base_classes = "rounded-md px-3 py-2 text-sm font-medium flex items-center flex-1 justify-center whitespace-nowrap hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors duration-150" selected_class = if assigns[:selected] do "bg-gray-150 dark:bg-gray-700" else "bg-transparent cursor-pointer" end assigns = assign(assigns, class: "#{selected_class} #{base_classes}") ~H""" <.link patch={@patch} class={@class}> {render_slot(@inner_block)} """ end def handle_event("submit", %{"tracker_script_configuration" => params}, socket) do config = PlausibleWeb.Tracker.update_script_configuration!( socket.assigns.site, params, :installation ) {:noreply, push_navigate(socket, to: Routes.site_path(socket, :verification, socket.assigns.site.domain, flow: socket.assigns.flow, installation_type: config.installation_type ) )} end defp initialize_installation_data(flow, site, params) do {recommended_installation_type, v1_detected} = detect_recommended_installation_type(flow, site) tracker_script_configuration = PlausibleWeb.Tracker.get_or_create_tracker_script_configuration!(site, %{ outbound_links: true, form_submissions: true, file_downloads: true, track_404_pages: true, installation_type: recommended_installation_type }) selected_installation_type = cond do params["type"] in PlausibleWeb.Tracker.supported_installation_types() -> params["type"] flow == Flows.review() and not is_nil(tracker_script_configuration.installation_type) -> Atom.to_string(tracker_script_configuration.installation_type) true -> recommended_installation_type end {:ok, %{ recommended_installation_type: recommended_installation_type, v1_detected: v1_detected, installation_type: selected_installation_type, tracker_script_configuration_form: to_form( Plausible.Site.TrackerScriptConfiguration.installation_changeset( tracker_script_configuration, %{} ) ) }} end end