Onboarding improvements (#4459)

* Migration: add installation meta

* Update site schema with installation meta

* Remove VERIFICATION_ENABLED env var

* Add context API to create/remove special goals

* Add context api to update installation meta

* Remove verification enabled check

* Update new progress flow definitions

* Update generic components

* Remove internal /status API

* Implement installation live view

* Update traffic change notifier link

* Update verification, no more modal

* Update routes

* Remove focus.html - will unify everything under app layout

* Fix broken link

* Update templates with focus_box mostly

* Update controller tests

* Update controllers and stop using the focus layout

* copy changes

* Update verification.ex

* Remove dead template

* Update settings_general.html.heex

* Update copy in tests

* Update installation.ex

* Remove dangling dot

* Fix link

* Update installation.ex

* Update installation.ex

* Better tooltips?

* Simpler labels

* Revert "Simpler labels"

This reverts commit 797560ef82f2067458b03b884be5aecc8fdc72bc.

* Add copy to clipboard link and fix snippet's dark mode

* Offer installation detection skip only if ws connected

* Put COPY link at the bottom with background

* Make tooltips link to docs

* Fix cherry-pick gone wrong

* Hide tooltips on mobile screens

* WIP: 404 tracking wizard

* Revert "WIP: 404 tracking wizard"

This reverts commit a9c9c79bbd.

* Update lib/plausible_web/live/components/verification.ex

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

* Update lib/plausible_web/live/installation.ex

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

* Use current_user from socket.assigns

* Update lib/plausible_web/live/installation.ex

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

* Use current_user from socket.assigns

* Use conn.private to steer verification tests

* Drop non-sticky tooltip in favour of component parametrization

Co-authored-by: Artur Pata <artur.pata@gmail.com>

* Reapply "WIP: 404 tracking wizard"

This reverts commit 3ba81671d7.

* Fix installation tests including 404 tracking

* Fixup the tooltip component

* Format

* Update installation.ex

* Put flash whenever installation option changes

* Use last known installation type on domain change

* Extract user flow definition to provide compile-time checks

* See if this helps running CE migrations successfully

* Use `styled_link` on registration/login views

* Don't crash when there's no conn.private carried over

* Format

* Push "Determining installation type" message a bit lower

* Use links and footer lists uniformly

This commit introduces a `<.focus_list/>` component
for rendering focus box footer links with colored
discs. It also equips generic link components
with the ability of sending non-GET requests
along with CSRF token, so we can apply uniform
styling and stop using legacy Phoenix link tags.

cc @zoldar @apata

* ws 👾

* Render more descriptive flashes on script config change

---------

Co-authored-by: Marko Saric <34340819+metmarkosaric@users.noreply.github.com>
Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
Co-authored-by: Artur Pata <artur.pata@gmail.com>
This commit is contained in:
hq1 2024-09-02 12:49:54 +02:00 committed by GitHub
parent f04c47f881
commit e3af1a317d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 2044 additions and 1409 deletions

View File

@ -778,11 +778,6 @@ config :plausible, Plausible.PromEx,
grafana: :disabled, grafana: :disabled,
metrics_server: :disabled metrics_server: :disabled
config :plausible, Plausible.Verification,
enabled?:
get_var_from_path_or_env(config_dir, "VERIFICATION_ENABLED", "false")
|> String.to_existing_atom()
config :plausible, Plausible.Verification.Checks.Installation, config :plausible, Plausible.Verification.Checks.Installation,
token: get_var_from_path_or_env(config_dir, "BROWSERLESS_TOKEN", "dummy_token"), token: get_var_from_path_or_env(config_dir, "BROWSERLESS_TOKEN", "dummy_token"),
endpoint: get_var_from_path_or_env(config_dir, "BROWSERLESS_ENDPOINT", "http://0.0.0.0:3000") endpoint: get_var_from_path_or_env(config_dir, "BROWSERLESS_ENDPOINT", "http://0.0.0.0:3000")

View File

@ -29,6 +29,7 @@ defmodule Plausible.DataMigration.SiteImports do
sites_with_only_legacy_import = sites_with_only_legacy_import =
from(s in Site, from(s in Site,
as: :site, as: :site,
select: %{id: s.id, imported_data: s.imported_data},
where: where:
not is_nil(s.imported_data) and fragment("?->>'status'", s.imported_data) == "ok" and not is_nil(s.imported_data) and fragment("?->>'status'", s.imported_data) == "ok" and
not exists(site_import_query) not exists(site_import_query)

View File

@ -236,6 +236,57 @@ defmodule Plausible.Goals do
) )
end end
@spec create_outbound_links(Plausible.Site.t()) :: :ok
def create_outbound_links(%Plausible.Site{} = site) do
create(site, %{"event_name" => "Outbound Link: Click"}, upsert?: true)
:ok
end
@spec create_file_downloads(Plausible.Site.t()) :: :ok
def create_file_downloads(%Plausible.Site{} = site) do
create(site, %{"event_name" => "File Download"}, upsert?: true)
:ok
end
@spec create_404(Plausible.Site.t()) :: :ok
def create_404(%Plausible.Site{} = site) do
create(site, %{"event_name" => "404"}, upsert?: true)
:ok
end
@spec delete_outbound_links(Plausible.Site.t()) :: :ok
def delete_outbound_links(%Plausible.Site{} = site) do
q =
from g in Goal,
where: g.site_id == ^site.id,
where: g.event_name == "Outbound Link: Click"
Repo.delete_all(q)
:ok
end
@spec delete_file_downloads(Plausible.Site.t()) :: :ok
def delete_file_downloads(%Plausible.Site{} = site) do
q =
from g in Goal,
where: g.site_id == ^site.id,
where: g.event_name == "File Download"
Repo.delete_all(q)
:ok
end
@spec delete_404(Plausible.Site.t()) :: :ok
def delete_404(%Plausible.Site{} = site) do
q =
from g in Goal,
where: g.site_id == ^site.id,
where: g.event_name == "404"
Repo.delete_all(q)
:ok
end
defp insert_goal(site, params, upsert?) do defp insert_goal(site, params, upsert?) do
params = Map.delete(params, "site_id") params = Map.delete(params, "site_id")

View File

@ -32,6 +32,10 @@ defmodule Plausible.Site do
# NOTE: needed by `SiteImports` data migration script # NOTE: needed by `SiteImports` data migration script
embeds_one :imported_data, Plausible.Site.ImportedData, on_replace: :update embeds_one :imported_data, Plausible.Site.ImportedData, on_replace: :update
embeds_one :installation_meta, Plausible.Site.InstallationMeta,
on_replace: :update,
defaults_to_struct: true
many_to_many :members, User, join_through: Plausible.Site.Membership many_to_many :members, User, join_through: Plausible.Site.Membership
has_many :memberships, Plausible.Site.Membership has_many :memberships, Plausible.Site.Membership
has_many :invitations, Plausible.Auth.Invitation has_many :invitations, Plausible.Auth.Invitation

View File

@ -0,0 +1,13 @@
defmodule Plausible.Site.InstallationMeta do
@moduledoc """
Embedded schema for installation meta-data
"""
use Ecto.Schema
@type t() :: %__MODULE__{}
embedded_schema do
field :installation_type, :string, default: "manual"
field :script_config, :map, default: %{}
end
end

View File

@ -327,6 +327,13 @@ defmodule Plausible.Sites do
end end
end end
def update_installation_meta!(site, meta) do
site
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_change(:installation_meta, meta)
|> Repo.update!()
end
defp get_for_user_q(user_id, domain, roles) do defp get_for_user_q(user_id, domain, roles) do
from(s in Site, from(s in Site,
join: sm in Site.Membership, join: sm in Site.Membership,

View File

@ -4,10 +4,6 @@ defmodule Plausible.Verification do
""" """
use Plausible use Plausible
def enabled?() do
:plausible |> Application.fetch_env!(__MODULE__) |> Keyword.fetch!(:enabled?)
end
on_ee do on_ee do
def user_agent() do def user_agent() do
"Plausible Verification Agent - if abused, contact support@plausible.io" "Plausible Verification Agent - if abused, contact support@plausible.io"

View File

@ -1,38 +1,16 @@
defmodule PlausibleWeb.Components.FlowProgress do defmodule PlausibleWeb.Components.FlowProgress do
@moduledoc """ @moduledoc """
Component for provisioning/registration flows displaying Component for provisioning/registration flows displaying
progress status. progress status. See `PlausibleWeb.Flows` for the list of
flow definitions.
""" """
use Phoenix.Component use Phoenix.Component
@flows %{ attr :flow, :string, required: true, values: PlausibleWeb.Flows.valid_keys()
"register" => [ attr :current_step, :string, required: true, values: PlausibleWeb.Flows.valid_values()
"Register",
"Activate account",
"Add site info",
"Install snippet",
"Verify snippet"
],
"invitation" => [
"Register",
"Activate account"
],
"provisioning" => [
"Add site info",
"Install snippet",
"Verify snippet"
]
}
@values @flows |> Enum.flat_map(fn {_, steps} -> steps end) |> Enum.uniq()
def flows, do: @flows
attr :flow, :string, required: true
attr :current_step, :string, required: true, values: @values
def render(assigns) do def render(assigns) do
steps = Map.get(flows(), assigns.flow, []) steps = PlausibleWeb.Flows.steps(assigns.flow)
current_step_idx = Enum.find_index(steps, &(&1 == assigns.current_step)) current_step_idx = Enum.find_index(steps, &(&1 == assigns.current_step))
assigns = assigns =

View File

@ -181,6 +181,7 @@ defmodule PlausibleWeb.Components.Generic do
attr :new_tab, :boolean, default: false attr :new_tab, :boolean, default: false
attr :class, :string, default: "" attr :class, :string, default: ""
attr :rest, :global attr :rest, :global
attr :method, :string, default: "get"
slot :inner_block slot :inner_block
def styled_link(assigns) do def styled_link(assigns) do
@ -188,6 +189,7 @@ defmodule PlausibleWeb.Components.Generic do
<.unstyled_link <.unstyled_link
new_tab={@new_tab} new_tab={@new_tab}
href={@href} href={@href}
method={@method}
class={"text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-600 " <> @class} class={"text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-600 " <> @class}
{@rest} {@rest}
> >
@ -263,9 +265,23 @@ defmodule PlausibleWeb.Components.Generic do
attr :class, :string, default: "" attr :class, :string, default: ""
attr :id, :any, default: nil attr :id, :any, default: nil
attr :rest, :global attr :rest, :global
attr :method, :string, default: "get"
slot :inner_block slot :inner_block
def unstyled_link(assigns) do def unstyled_link(assigns) do
extra =
if assigns.method == "get" do
[]
else
[
"data-csrf": Phoenix.HTML.Tag.csrf_token_value(assigns.href),
"data-method": assigns.method,
"data-to": assigns.href
]
end
assigns = assign(assigns, extra: extra)
if assigns[:new_tab] do if assigns[:new_tab] do
assigns = assign(assigns, :icon_class, icon_class(assigns)) assigns = assign(assigns, :icon_class, icon_class(assigns))
@ -279,6 +295,7 @@ defmodule PlausibleWeb.Components.Generic do
href={@href} href={@href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
{@extra}
{@rest} {@rest}
> >
<%= render_slot(@inner_block) %> <%= render_slot(@inner_block) %>
@ -287,7 +304,7 @@ defmodule PlausibleWeb.Components.Generic do
""" """
else else
~H""" ~H"""
<.link class={@class} href={@href} {@rest}> <.link class={@class} href={@href} {@extra} {@rest}>
<%= render_slot(@inner_block) %> <%= render_slot(@inner_block) %>
</.link> </.link>
""" """
@ -320,12 +337,22 @@ defmodule PlausibleWeb.Components.Generic do
attr :wrapper_class, :any, default: "" attr :wrapper_class, :any, default: ""
attr :class, :any, default: "" attr :class, :any, default: ""
attr :icon?, :boolean, default: true
attr :sticky?, :boolean, default: true
attr :position, :string, default: "bottom-10 margin-x-auto left-10 right-10"
slot :inner_block, required: true slot :inner_block, required: true
slot :tooltip_content, required: true slot :tooltip_content, required: true
def tooltip(assigns) do def tooltip(assigns) do
wrapper_data =
if assigns[:sticky?], do: "{sticky: false, hovered: false}", else: "{hovered: false}"
show_inner = if assigns[:sticky?], do: "hovered || sticky", else: "hovered"
assigns = assign(assigns, wrapper_data: wrapper_data, show_inner: show_inner)
~H""" ~H"""
<div x-data="{sticky: false, hovered: false}" class={["tooltip-wrapper relative", @wrapper_class]}> <div x-data={@wrapper_data} class={["tooltip-wrapper relative", @wrapper_class]}>
<p <p
x-on:click="sticky = true; hovered = true" x-on:click="sticky = true; hovered = true"
x-on:click.outside="sticky = false; hovered = false" x-on:click.outside="sticky = false; hovered = false"
@ -334,11 +361,14 @@ defmodule PlausibleWeb.Components.Generic do
class={["cursor-pointer flex align-items-center", @class]} class={["cursor-pointer flex align-items-center", @class]}
> >
<%= render_slot(@inner_block) %> <%= render_slot(@inner_block) %>
<Heroicons.information_circle class="w-5 h-5 ml-2" /> <Heroicons.information_circle :if={@icon?} class="w-5 h-5 ml-2" />
</p> </p>
<span <span
x-show="hovered || sticky" x-show={@show_inner}
class="bg-gray-900 pointer-events-none absolute bottom-10 margin-x-auto left-10 right-10 transition-opacity p-4 rounded text-sm text-white" class={[
"bg-gray-900 pointer-events-none absolute transition-opacity p-4 rounded text-sm text-white",
@position
]}
> >
<%= render_slot(List.first(@tooltip_content)) %> <%= render_slot(List.first(@tooltip_content)) %>
</span> </span>
@ -369,20 +399,27 @@ defmodule PlausibleWeb.Components.Generic do
end end
end end
slot :item, required: true
def focus_list(assigns) do
~H"""
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4">
<li :for={item <- @item} class="marker:text-indigo-700 dark:marker:text-indigo-700">
<%= render_slot(item) %>
</li>
</ol>
"""
end
slot :title slot :title
slot :subtitle slot :subtitle
slot :inner_block, required: true slot :inner_block, required: true
slot :footer slot :footer
attr :outer_markup, :boolean, default: true
def focus_box(assigns) do def focus_box(assigns) do
~H""" ~H"""
<div class={[ <div class="focus-box bg-white w-full max-w-lg mx-auto dark:bg-gray-800 text-black dark:text-gray-100 shadow-md rounded mb-4 mt-8">
"bg-white w-full max-w-lg mx-auto dark:bg-gray-800 text-black dark:text-gray-100", <div class="p-8">
@outer_markup && "shadow-md rounded mb-4 mt-8"
]}>
<div class={[@outer_markup && "p-8"]}>
<h2 :if={@title != []} class="text-xl font-black dark:text-gray-100"> <h2 :if={@title != []} class="text-xl font-black dark:text-gray-100">
<%= render_slot(@title) %> <%= render_slot(@title) %>
</h2> </h2>
@ -403,7 +440,7 @@ defmodule PlausibleWeb.Components.Generic do
:if={@footer != []} :if={@footer != []}
class="flex flex-col dark:text-gray-200 border-t border-gray-300 dark:border-gray-700" class="flex flex-col dark:text-gray-200 border-t border-gray-300 dark:border-gray-700"
> >
<div class={[@outer_markup && "p-8"]}> <div class="p-8">
<%= render_slot(@footer) %> <%= render_slot(@footer) %>
</div> </div>
</div> </div>

View File

@ -1,22 +1,9 @@
defmodule PlausibleWeb.Api.InternalController do defmodule PlausibleWeb.Api.InternalController do
use PlausibleWeb, :controller use PlausibleWeb, :controller
use Plausible.Repo use Plausible.Repo
alias Plausible.Stats.Clickhouse, as: Stats alias Plausible.{Sites, Auth}
alias Plausible.{Sites, Site, Auth}
alias Plausible.Auth.User alias Plausible.Auth.User
def domain_status(conn, %{"domain" => domain}) do
with %User{id: user_id} <- conn.assigns[:current_user],
%Site{} = site <- Sites.get_by_domain(domain),
true <- Sites.has_admin_access?(user_id, site) || Auth.is_super_admin?(user_id),
true <- Stats.has_pageviews?(site) do
json(conn, "READY")
else
_ ->
json(conn, "WAITING")
end
end
def sites(conn, _params) do def sites(conn, _params) do
current_user = conn.assigns[:current_user] current_user = conn.assigns[:current_user]

View File

@ -62,12 +62,11 @@ defmodule PlausibleWeb.AuthController do
def activate_form(conn, params) do def activate_form(conn, params) do
user = conn.assigns.current_user user = conn.assigns.current_user
flow = params["flow"] || "register" flow = params["flow"] || PlausibleWeb.Flows.register()
render(conn, "activate.html", render(conn, "activate.html",
has_email_code?: Plausible.Users.has_email_code?(user), has_email_code?: Plausible.Users.has_email_code?(user),
has_any_memberships?: Plausible.Site.Memberships.any?(user), has_any_memberships?: Plausible.Site.Memberships.any?(user),
layout: {PlausibleWeb.LayoutView, "focus.html"},
form_submit_url: "/activate?flow=#{flow}" form_submit_url: "/activate?flow=#{flow}"
) )
end end
@ -98,7 +97,6 @@ defmodule PlausibleWeb.AuthController do
error: "Incorrect activation code", error: "Incorrect activation code",
has_email_code?: true, has_email_code?: true,
has_any_memberships?: has_any_memberships?, has_any_memberships?: has_any_memberships?,
layout: {PlausibleWeb.LayoutView, "focus.html"},
form_submit_url: "/activate?flow=#{flow}" form_submit_url: "/activate?flow=#{flow}"
) )
@ -107,7 +105,6 @@ defmodule PlausibleWeb.AuthController do
error: "Code is expired, please request another one", error: "Code is expired, please request another one",
has_email_code?: false, has_email_code?: false,
has_any_memberships?: has_any_memberships?, has_any_memberships?: has_any_memberships?,
layout: {PlausibleWeb.LayoutView, "focus.html"},
form_submit_url: "/activate?flow=#{flow}" form_submit_url: "/activate?flow=#{flow}"
) )
end end
@ -123,16 +120,11 @@ defmodule PlausibleWeb.AuthController do
end end
def password_reset_request_form(conn, _) do def password_reset_request_form(conn, _) do
render(conn, "password_reset_request_form.html", render(conn, "password_reset_request_form.html")
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end end
def password_reset_request(conn, %{"email" => ""}) do def password_reset_request(conn, %{"email" => ""}) do
render(conn, "password_reset_request_form.html", render(conn, "password_reset_request_form.html", error: "Please enter an email address")
error: "Please enter an email address",
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end end
def password_reset_request(conn, %{"email" => email} = params) do def password_reset_request(conn, %{"email" => email} = params) do
@ -149,20 +141,13 @@ defmodule PlausibleWeb.AuthController do
"Password reset e-mail sent. In dev environment GET /sent-emails for details." "Password reset e-mail sent. In dev environment GET /sent-emails for details."
) )
render(conn, "password_reset_request_success.html", render(conn, "password_reset_request_success.html", email: email)
email: email,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
else else
render(conn, "password_reset_request_success.html", render(conn, "password_reset_request_success.html", email: email)
email: email,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end end
else else
render(conn, "password_reset_request_form.html", render(conn, "password_reset_request_form.html",
error: "Please complete the captcha to reset your password", error: "Please complete the captcha to reset your password"
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
end end
@ -172,8 +157,7 @@ defmodule PlausibleWeb.AuthController do
{:ok, %{email: email}} -> {:ok, %{email: email}} ->
render(conn, "password_reset_form.html", render(conn, "password_reset_form.html",
connect_live_socket: true, connect_live_socket: true,
email: email, email: email
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
{:error, :expired} -> {:error, :expired} ->
@ -201,7 +185,7 @@ defmodule PlausibleWeb.AuthController do
end end
def login_form(conn, _params) do def login_form(conn, _params) do
render(conn, "login_form.html", layout: {PlausibleWeb.LayoutView, "focus.html"}) render(conn, "login_form.html")
end end
def login(conn, %{"user" => params}) do def login(conn, %{"user" => params}) do
@ -221,9 +205,9 @@ defmodule PlausibleWeb.AuthController do
flow = flow =
if params["register_action"] == "register_form" do if params["register_action"] == "register_form" do
"register" PlausibleWeb.Flows.register()
else else
"invitation" PlausibleWeb.Flows.invitation()
end end
Routes.auth_path(conn, :activate_form, flow: flow) Routes.auth_path(conn, :activate_form, flow: flow)
@ -243,19 +227,13 @@ defmodule PlausibleWeb.AuthController do
{:error, :wrong_password} -> {:error, :wrong_password} ->
maybe_log_failed_login_attempts("wrong password for #{email}") maybe_log_failed_login_attempts("wrong password for #{email}")
render(conn, "login_form.html", render(conn, "login_form.html", error: "Wrong email or password. Please try again.")
error: "Wrong email or password. Please try again.",
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
{:error, :user_not_found} -> {:error, :user_not_found} ->
maybe_log_failed_login_attempts("user not found for #{email}") maybe_log_failed_login_attempts("user not found for #{email}")
Plausible.Auth.Password.dummy_calculation() Plausible.Auth.Password.dummy_calculation()
render(conn, "login_form.html", render(conn, "login_form.html", error: "Wrong email or password. Please try again.")
error: "Wrong email or password. Please try again.",
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
{:error, {:rate_limit, _}} -> {:error, {:rate_limit, _}} ->
maybe_log_failed_login_attempts("too many login attempts for #{email}") maybe_log_failed_login_attempts("too many login attempts for #{email}")
@ -370,8 +348,7 @@ defmodule PlausibleWeb.AuthController do
{:ok, user} -> {:ok, user} ->
if Auth.TOTP.enabled?(user) do if Auth.TOTP.enabled?(user) do
render(conn, "verify_2fa.html", render(conn, "verify_2fa.html",
remember_2fa_days: TwoFactor.Session.remember_2fa_days(), remember_2fa_days: TwoFactor.Session.remember_2fa_days()
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
else else
redirect_to_login(conn) redirect_to_login(conn)
@ -398,8 +375,7 @@ defmodule PlausibleWeb.AuthController do
conn conn
|> put_flash(:error, "The provided code is invalid. Please try again") |> put_flash(:error, "The provided code is invalid. Please try again")
|> render("verify_2fa.html", |> render("verify_2fa.html",
remember_2fa_days: TwoFactor.Session.remember_2fa_days(), remember_2fa_days: TwoFactor.Session.remember_2fa_days()
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
{:error, :not_enabled} -> {:error, :not_enabled} ->
@ -412,9 +388,7 @@ defmodule PlausibleWeb.AuthController do
case TwoFactor.Session.get_2fa_user(conn) do case TwoFactor.Session.get_2fa_user(conn) do
{:ok, user} -> {:ok, user} ->
if Auth.TOTP.enabled?(user) do if Auth.TOTP.enabled?(user) do
render(conn, "verify_2fa_recovery_code.html", render(conn, "verify_2fa_recovery_code.html")
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
else else
redirect_to_login(conn) redirect_to_login(conn)
end end
@ -435,9 +409,7 @@ defmodule PlausibleWeb.AuthController do
conn conn
|> put_flash(:error, "The provided recovery code is invalid. Please try another one") |> put_flash(:error, "The provided recovery code is invalid. Please try another one")
|> render("verify_2fa_recovery_code.html", |> render("verify_2fa_recovery_code.html")
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
{:error, :not_enabled} -> {:error, :not_enabled} ->
UserAuth.log_in_user(conn, user) UserAuth.log_in_user(conn, user)
@ -577,10 +549,7 @@ defmodule PlausibleWeb.AuthController do
def new_api_key(conn, _params) do def new_api_key(conn, _params) do
changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}) changeset = Auth.ApiKey.changeset(%Auth.ApiKey{})
render(conn, "new_api_key.html", render(conn, "new_api_key.html", changeset: changeset)
changeset: changeset,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end end
def create_api_key(conn, %{"api_key" => %{"name" => name, "key" => key}}) do def create_api_key(conn, %{"api_key" => %{"name" => name, "key" => key}}) do
@ -591,10 +560,7 @@ defmodule PlausibleWeb.AuthController do
|> redirect(to: "/settings#api-keys") |> redirect(to: "/settings#api-keys")
{:error, changeset} -> {:error, changeset} ->
render(conn, "new_api_key.html", render(conn, "new_api_key.html", changeset: changeset)
changeset: changeset,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end end
end end

View File

@ -21,7 +21,6 @@ defmodule PlausibleWeb.BillingController do
else else
render(conn, "choose_plan.html", render(conn, "choose_plan.html",
skip_plausible_tracking: true, skip_plausible_tracking: true,
layout: {PlausibleWeb.LayoutView, "focus.html"},
connect_live_socket: true connect_live_socket: true
) )
end end
@ -48,10 +47,7 @@ defmodule PlausibleWeb.BillingController do
redirect(conn, to: Routes.auth_path(conn, :user_settings)) redirect(conn, to: Routes.auth_path(conn, :user_settings))
subscribed_to_latest? -> subscribed_to_latest? ->
render(conn, "change_enterprise_plan_contact_us.html", render(conn, "change_enterprise_plan_contact_us.html", skip_plausible_tracking: true)
skip_plausible_tracking: true,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
true -> true ->
render(conn, "upgrade_to_enterprise_plan.html", render(conn, "upgrade_to_enterprise_plan.html",
@ -59,14 +55,13 @@ defmodule PlausibleWeb.BillingController do
price: price, price: price,
subscription_resumable: subscription_resumable?, subscription_resumable: subscription_resumable?,
contact_link: "https://plausible.io/contact", contact_link: "https://plausible.io/contact",
skip_plausible_tracking: true, skip_plausible_tracking: true
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
end end
def upgrade_success(conn, _params) do def upgrade_success(conn, _params) do
render(conn, "upgrade_success.html", layout: {PlausibleWeb.LayoutView, "focus.html"}) render(conn, "upgrade_success.html")
end end
def change_plan_preview(conn, %{"plan_id" => new_plan_id}) do def change_plan_preview(conn, %{"plan_id" => new_plan_id}) do
@ -78,8 +73,7 @@ defmodule PlausibleWeb.BillingController do
back_link: Routes.billing_path(conn, :choose_plan), back_link: Routes.billing_path(conn, :choose_plan),
skip_plausible_tracking: true, skip_plausible_tracking: true,
subscription: subscription, subscription: subscription,
preview_info: preview_info, preview_info: preview_info
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
_ -> _ ->

View File

@ -43,8 +43,7 @@ defmodule PlausibleWeb.DebugController do
conn conn
|> render("clickhouse.html", |> render("clickhouse.html",
queries: queries, queries: queries
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end

View File

@ -46,8 +46,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
expires_at: expires_at, expires_at: expires_at,
site: conn.assigns.site, site: conn.assigns.site,
properties: properties, properties: properties,
selected_property_error: error, selected_property_error: error
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
{:error, :rate_limit_exceeded} -> {:error, :rate_limit_exceeded} ->
@ -182,8 +181,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
selected_property: property, selected_property: property,
selected_property_name: property_name, selected_property_name: property_name,
start_date: start_date, start_date: start_date,
end_date: end_date, end_date: end_date
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
{:error, :rate_limit_exceeded} -> {:error, :rate_limit_exceeded} ->

View File

@ -10,6 +10,6 @@ defmodule PlausibleWeb.PageController do
This controller action is only ever triggered in self-hosted Plausible. This controller action is only ever triggered in self-hosted Plausible.
""" """
def index(conn, _params) do def index(conn, _params) do
render(conn, "index.html", layout: {PlausibleWeb.LayoutView, "focus.html"}) render(conn, "index.html")
end end
end end

View File

@ -37,7 +37,6 @@ defmodule PlausibleWeb.Site.MembershipController do
conn, conn,
"invite_member_form.html", "invite_member_form.html",
site: site, site: site,
layout: {PlausibleWeb.LayoutView, "focus.html"},
team_member_limit: limit, team_member_limit: limit,
is_at_limit: not below_limit?, is_at_limit: not below_limit?,
skip_plausible_tracking: true skip_plausible_tracking: true
@ -64,7 +63,6 @@ defmodule PlausibleWeb.Site.MembershipController do
render(conn, "invite_member_form.html", render(conn, "invite_member_form.html",
error: "Cannot send invite because #{email} is already a member of #{site.domain}", error: "Cannot send invite because #{email} is already a member of #{site.domain}",
site: site, site: site,
layout: {PlausibleWeb.LayoutView, "focus.html"},
skip_plausible_tracking: true skip_plausible_tracking: true
) )
@ -73,7 +71,6 @@ defmodule PlausibleWeb.Site.MembershipController do
error: error:
"Your account is limited to #{limit} team members. You can upgrade your plan to increase this limit.", "Your account is limited to #{limit} team members. You can upgrade your plan to increase this limit.",
site: site, site: site,
layout: {PlausibleWeb.LayoutView, "focus.html"},
skip_plausible_tracking: true, skip_plausible_tracking: true,
is_at_limit: true, is_at_limit: true,
team_member_limit: limit team_member_limit: limit
@ -103,7 +100,6 @@ defmodule PlausibleWeb.Site.MembershipController do
conn, conn,
"transfer_ownership_form.html", "transfer_ownership_form.html",
site: site, site: site,
layout: {PlausibleWeb.LayoutView, "focus.html"},
skip_plausible_tracking: true skip_plausible_tracking: true
) )
end end

View File

@ -14,14 +14,13 @@ defmodule PlausibleWeb.SiteController do
) )
def new(conn, params) do def new(conn, params) do
flow = params["flow"] || "register" flow = params["flow"] || PlausibleWeb.Flows.register()
current_user = conn.assigns[:current_user] current_user = conn.assigns[:current_user]
render(conn, "new.html", render(conn, "new.html",
changeset: Plausible.Site.changeset(%Plausible.Site{}), changeset: Plausible.Site.changeset(%Plausible.Site{}),
site_limit: Quota.Limits.site_limit(current_user), site_limit: Quota.Limits.site_limit(current_user),
site_limit_exceeded?: Quota.ensure_can_add_new_site(current_user) != :ok, site_limit_exceeded?: Quota.ensure_can_add_new_site(current_user) != :ok,
layout: {PlausibleWeb.LayoutView, "focus.html"},
form_submit_url: "/sites?flow=#{flow}", form_submit_url: "/sites?flow=#{flow}",
flow: flow flow: flow
) )
@ -41,7 +40,7 @@ defmodule PlausibleWeb.SiteController do
redirect(conn, redirect(conn,
external: external:
Routes.site_path(conn, :add_snippet, site.domain, Routes.site_path(conn, :installation, site.domain,
site_created: true, site_created: true,
flow: flow flow: flow
) )
@ -53,7 +52,6 @@ defmodule PlausibleWeb.SiteController do
first_site?: first_site?, first_site?: first_site?,
site_limit: limit, site_limit: limit,
site_limit_exceeded?: true, site_limit_exceeded?: true,
layout: {PlausibleWeb.LayoutView, "focus.html"},
flow: flow, flow: flow,
form_submit_url: "/sites?flow=#{flow}" form_submit_url: "/sites?flow=#{flow}"
) )
@ -64,37 +62,12 @@ defmodule PlausibleWeb.SiteController do
first_site?: first_site?, first_site?: first_site?,
site_limit: Quota.Limits.site_limit(user), site_limit: Quota.Limits.site_limit(user),
site_limit_exceeded?: false, site_limit_exceeded?: false,
layout: {PlausibleWeb.LayoutView, "focus.html"},
flow: flow, flow: flow,
form_submit_url: "/sites?flow=#{flow}" form_submit_url: "/sites?flow=#{flow}"
) )
end end
end end
def add_snippet(conn, params) do
flow = params["flow"] || "register"
user = conn.assigns[:current_user]
site = conn.assigns[:site]
is_first_site =
!Repo.exists?(
from(sm in Plausible.Site.Membership,
where:
sm.user_id == ^user.id and
sm.site_id != ^site.id
)
)
conn
|> render("snippet.html",
site: site,
skip_plausible_tracking: true,
is_first_site: is_first_site,
layout: {PlausibleWeb.LayoutView, "focus.html"},
form_submit_url: "/#{URI.encode_www_form(site.domain)}?flow=#{flow}"
)
end
def update_feature_visibility(conn, %{ def update_feature_visibility(conn, %{
"setting" => setting, "setting" => setting,
"r" => "/" <> _ = redirect_path, "r" => "/" <> _ = redirect_path,
@ -585,8 +558,7 @@ defmodule PlausibleWeb.SiteController do
|> assign(:skip_plausible_tracking, true) |> assign(:skip_plausible_tracking, true)
|> render("new_shared_link.html", |> render("new_shared_link.html",
site: site, site: site,
changeset: changeset, changeset: changeset
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
@ -602,8 +574,7 @@ defmodule PlausibleWeb.SiteController do
|> assign(:skip_plausible_tracking, true) |> assign(:skip_plausible_tracking, true)
|> render("new_shared_link.html", |> render("new_shared_link.html",
site: site, site: site,
changeset: changeset, changeset: changeset
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
end end
@ -617,8 +588,7 @@ defmodule PlausibleWeb.SiteController do
|> assign(:skip_plausible_tracking, true) |> assign(:skip_plausible_tracking, true)
|> render("edit_shared_link.html", |> render("edit_shared_link.html",
site: site, site: site,
changeset: changeset, changeset: changeset
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
@ -636,8 +606,7 @@ defmodule PlausibleWeb.SiteController do
|> assign(:skip_plausible_tracking, true) |> assign(:skip_plausible_tracking, true)
|> render("edit_shared_link.html", |> render("edit_shared_link.html",
site: site, site: site,
changeset: changeset, changeset: changeset
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
end end
@ -750,7 +719,6 @@ defmodule PlausibleWeb.SiteController do
conn conn
|> assign(:skip_plausible_tracking, true) |> assign(:skip_plausible_tracking, true)
|> render("csv_import.html", |> render("csv_import.html",
layout: {PlausibleWeb.LayoutView, "focus.html"},
connect_live_socket: true connect_live_socket: true
) )
end end
@ -760,8 +728,7 @@ defmodule PlausibleWeb.SiteController do
render(conn, "change_domain.html", render(conn, "change_domain.html",
skip_plausible_tracking: true, skip_plausible_tracking: true,
changeset: changeset, changeset: changeset
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
@ -771,29 +738,20 @@ defmodule PlausibleWeb.SiteController do
conn conn
|> put_flash(:success, "Website domain changed successfully") |> put_flash(:success, "Website domain changed successfully")
|> redirect( |> redirect(
external: Routes.site_path(conn, :add_snippet_after_domain_change, updated_site.domain) external:
Routes.site_path(conn, :installation, updated_site.domain,
flow: PlausibleWeb.Flows.domain_change()
)
) )
{:error, changeset} -> {:error, changeset} ->
render(conn, "change_domain.html", render(conn, "change_domain.html",
skip_plausible_tracking: true, skip_plausible_tracking: true,
changeset: changeset, changeset: changeset
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
end end
def add_snippet_after_domain_change(conn, _params) do
site = conn.assigns[:site]
conn
|> assign(:skip_plausible_tracking, true)
|> render("snippet_after_domain_change.html",
site: site,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
defp tolerate_unique_contraint_violation(result, name) do defp tolerate_unique_contraint_violation(result, name) do
case result do case result do
{:ok, _} -> {:ok, _} ->

View File

@ -79,20 +79,7 @@ defmodule PlausibleWeb.StatsController do
) )
!stats_start_date && can_see_stats? -> !stats_start_date && can_see_stats? ->
render_opts = [ redirect(conn, external: Routes.site_path(conn, :verification, site.domain))
site: site,
dogfood_page_path: dogfood_page_path,
connect_live_socket: true
]
render_opts =
if conn.params["flow"] do
Keyword.put(render_opts, :layout, {PlausibleWeb.LayoutView, "focus.html"})
else
render_opts
end
render(conn, "waiting_first_pageview.html", render_opts)
Sites.locked?(site) -> Sites.locked?(site) ->
site = Plausible.Repo.preload(site, :owner) site = Plausible.Repo.preload(site, :owner)
@ -280,7 +267,6 @@ defmodule PlausibleWeb.StatsController do
conn conn
|> render("shared_link_password.html", |> render("shared_link_password.html",
link: shared_link, link: shared_link,
layout: {PlausibleWeb.LayoutView, "focus.html"},
dogfood_page_path: "/share/:dashboard" dogfood_page_path: "/share/:dashboard"
) )
end end
@ -325,7 +311,6 @@ defmodule PlausibleWeb.StatsController do
|> render("shared_link_password.html", |> render("shared_link_password.html",
link: shared_link, link: shared_link,
error: "Incorrect password. Please try again.", error: "Incorrect password. Please try again.",
layout: {PlausibleWeb.LayoutView, "focus.html"},
dogfood_page_path: "/share/:dashboard" dogfood_page_path: "/share/:dashboard"
) )
end end

View File

@ -17,8 +17,7 @@ defmodule PlausibleWeb.UnsubscribeController do
|> assign(:skip_plausible_tracking, true) |> assign(:skip_plausible_tracking, true)
|> render("success.html", |> render("success.html",
interval: "weekly", interval: "weekly",
site: website, site: site || %{domain: website}
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end
@ -40,8 +39,7 @@ defmodule PlausibleWeb.UnsubscribeController do
|> assign(:skip_plausible_tracking, true) |> assign(:skip_plausible_tracking, true)
|> render("success.html", |> render("success.html",
interval: "monthly", interval: "monthly",
site: website, site: site || %{domain: website}
layout: {PlausibleWeb.LayoutView, "focus.html"}
) )
end end

View File

@ -141,7 +141,7 @@ defmodule PlausibleWeb.Email do
}) })
end end
def drop_notification(email, site, current_visitors, dashboard_link, settings_link) do def drop_notification(email, site, current_visitors, dashboard_link, installation_link) do
base_email() base_email()
|> to(email) |> to(email)
|> tag("drop-notification") |> tag("drop-notification")
@ -150,7 +150,7 @@ defmodule PlausibleWeb.Email do
site: site, site: site,
current_visitors: current_visitors, current_visitors: current_visitors,
dashboard_link: dashboard_link, dashboard_link: dashboard_link,
settings_link: settings_link installation_link: installation_link
}) })
end end

View File

@ -0,0 +1,64 @@
defmodule PlausibleWeb.Flows do
@moduledoc """
Static compile-time definitions for user progress flows.
See `PlausibleWeb.Components.FlowProgress` for rendering capabilities.
"""
@flows %{
review: [
"Install Plausible",
"Verify installation"
],
domain_change: [
"Set up new domain",
"Install Plausible",
"Verify installation"
],
register: [
"Register",
"Activate account",
"Add site info",
"Install Plausible",
"Verify installation"
],
invitation: [
"Register",
"Activate account"
],
provisioning: [
"Add site info",
"Install Plausible",
"Verify installation"
]
}
@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
end

View File

@ -6,10 +6,11 @@ defmodule PlausibleWeb.Live.Components.Verification do
use Phoenix.LiveComponent use Phoenix.LiveComponent
use Plausible use Plausible
alias PlausibleWeb.Router.Helpers, as: Routes
import PlausibleWeb.Components.Generic import PlausibleWeb.Components.Generic
attr :domain, :string, required: true attr :domain, :string, required: true
attr :modal?, :boolean, default: false
attr :message, :string, default: "We're visiting your site to ensure that everything is working" attr :message, :string, default: "We're visiting your site to ensure that everything is working"
@ -18,20 +19,22 @@ defmodule PlausibleWeb.Live.Components.Verification do
attr :interpretation, Plausible.Verification.Diagnostics.Result, default: nil attr :interpretation, Plausible.Verification.Diagnostics.Result, default: nil
attr :attempts, :integer, default: 0 attr :attempts, :integer, default: 0
attr :flow, :string, default: "" attr :flow, :string, default: ""
attr :installation_type, :string, default: nil
attr :awaiting_first_pageview?, :boolean, default: false
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div id="progress-indicator"> <div id="progress-indicator">
<PlausibleWeb.Components.Generic.focus_box outer_markup={not @modal?}> <PlausibleWeb.Components.Generic.focus_box>
<div <div
:if={not @finished? or (not @modal? and @success?)} :if={not @finished?}
class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-gray-700" class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-gray-700"
> >
<div class="block pulsating-circle"></div> <div class="block pulsating-circle"></div>
</div> </div>
<div <div
:if={@finished? and @success? and @modal?} :if={@finished? and @success?}
class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-500" class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-500"
id="check-circle" id="check-circle"
> >
@ -49,17 +52,21 @@ defmodule PlausibleWeb.Live.Components.Verification do
<div class="mt-6"> <div class="mt-6">
<h3 class="font-semibold leading-6 text-xl"> <h3 class="font-semibold leading-6 text-xl">
<span :if={@finished? and @success?}>Success!</span> <span :if={@finished? and @success?}>Success!</span>
<span :if={not @finished?}>Verifying your integration</span> <span :if={not @finished?}>Verifying your installation</span>
<span :if={@finished? and not @success? and @interpretation}> <span :if={@finished? and not @success? and @interpretation}>
<%= List.first(@interpretation.errors) %> <%= List.first(@interpretation.errors) %>
</span> </span>
</h3> </h3>
<p :if={@finished? and @success? and @modal?} id="progress" class="mt-2"> <p :if={@finished? and @success?} id="progress" class="mt-2">
Your integration is working and visitors are being counted accurately Your installation is working and visitors are being counted accurately
</p> </p>
<p :if={@finished? and @success? and not @modal?} id="progress" class="mt-2 animate-pulse"> <p
Your integration is working. Awaiting your first pageview. :if={@finished? and @success? and @awaiting_first_pageview?}
id="progress"
class="mt-2 animate-pulse"
>
Awaiting your first pageview
</p> </p>
<p :if={not @finished?} class="mt-2 animate-pulse" id="progress"><%= @message %></p> <p :if={not @finished?} class="mt-2 animate-pulse" id="progress"><%= @message %></p>
@ -77,7 +84,7 @@ defmodule PlausibleWeb.Live.Components.Verification do
<div :if={@finished?} class="mt-8"> <div :if={@finished?} class="mt-8">
<.button_link :if={not @success?} href="#" phx-click="retry" class="w-full"> <.button_link :if={not @success?} href="#" phx-click="retry" class="w-full">
Verify integration again Verify installation again
</.button_link> </.button_link>
<.button_link <.button_link
:if={@success?} :if={@success?}
@ -88,34 +95,32 @@ defmodule PlausibleWeb.Live.Components.Verification do
</.button_link> </.button_link>
</div> </div>
<:footer :if={ <:footer :if={@finished? and not @success?}>
(not @modal? and not @success?) or <.focus_list>
(@finished? and not @success?) <:item :if={ee?() and @attempts >= 3}>
}> <b>Need further help with your installation?</b>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4"> <.styled_link href="https://plausible.io/contact">
<%= if ee?() and @finished? and not @success? and @attempts >= 3 do %> Contact us
<li> </.styled_link>
<b>Need further help with your integration?</b> </:item>
<.styled_link href="https://plausible.io/contact"> <:item>
Contact us Need to see installation instructions again?
</.styled_link> <.styled_link href={
</li> Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain,
<% end %> flow: @flow,
<%= if not @success? and not @modal? do %> installation_type: @installation_type
<li> )
Need to see the snippet again? }>
<.styled_link href={"/#{URI.encode_www_form(@domain)}/snippet?flow=#{@flow}"}> Click here
Click here </.styled_link>
</.styled_link> </:item>
</li> <:item>
<li> Run verification later and go to site settings?
Run verification later and go to Site Settings? <.styled_link href={"/#{URI.encode_www_form(@domain)}/settings/general"}>
<.styled_link href={"/#{URI.encode_www_form(@domain)}/settings/general"}> Click here
Click here </.styled_link>
</.styled_link> </:item>
</li> </.focus_list>
<% end %>
</ol>
</:footer> </:footer>
</PlausibleWeb.Components.Generic.focus_box> </PlausibleWeb.Components.Generic.focus_box>
</div> </div>

View File

@ -0,0 +1,526 @@
defmodule PlausibleWeb.Live.Installation do
@moduledoc """
User assistance module around Plausible installation instructions/onboarding
"""
use PlausibleWeb, :live_view
use Phoenix.HTML
alias Plausible.Verification.{Checks, State}
import PlausibleWeb.Components.Generic
@script_extension_params [
"outbound-links",
"tagged-events",
"file-downloads",
"hash",
"pageview-props",
"revenue"
]
@script_config_params ["404" | @script_extension_params]
@installation_types [
"GTM",
"manual",
"WordPress"
]
@valid_qs_params @script_config_params ++ ["installation_type", "flow"]
def script_extension_params, do: @script_extension_params
def mount(
%{"website" => domain} = params,
_session,
socket
) do
site = Plausible.Sites.get_for_user!(socket.assigns.current_user, domain)
flow = params["flow"]
meta = site.installation_meta || %Plausible.Site.InstallationMeta{}
script_config =
@script_config_params
|> Enum.into(%{}, &{&1, false})
|> Map.merge(meta.script_config)
|> Map.take(@script_config_params)
installation_type = get_installation_type(flow, meta, params)
if connected?(socket) and is_nil(installation_type) do
Checks.run("https://#{domain}", domain,
checks: [
Checks.FetchBody,
Checks.ScanBody
],
report_to: self(),
async?: true,
slowdown: 0
)
end
{:ok,
assign(socket,
uri_params: Map.take(params, @valid_qs_params),
connected?: connected?(socket),
site: site,
site_created?: params["site_created"] == "true",
flow: flow,
installation_type: installation_type,
initial_installation_type: installation_type,
domain: domain,
script_config: script_config
)}
end
def handle_info({:verification_end, %State{} = state}, socket) do
installation_type =
case state.diagnostics do
%{wordpress_likely?: true} -> "WordPress"
%{gtm_likely?: true} -> "GTM"
_ -> "manual"
end
{:noreply,
assign(socket,
initial_installation_type: installation_type,
installation_type: installation_type
)}
end
def handle_info(_msg, socket) do
{:noreply, socket}
end
def render(assigns) do
~H"""
<div>
<.flash_messages flash={@flash} />
<PlausibleWeb.Components.FirstDashboardLaunchBanner.set :if={@site_created?} site={@site} />
<PlausibleWeb.Components.FlowProgress.render flow={@flow} current_step="Install Plausible" />
<PlausibleWeb.Components.Generic.focus_box>
<:title :if={is_nil(@installation_type)}>
<div class="flex w-full mx-auto justify-center">
<PlausibleWeb.Components.Generic.spinner class="spinner block text-center h-8 w-8" />
</div>
</:title>
<:title :if={@installation_type == "WordPress"}>
Install WordPress plugin
</:title>
<:title :if={@installation_type == "GTM"}>
Install Google Tag Manager
</:title>
<:title :if={@installation_type == "manual"}>
Manual installation
</:title>
<:subtitle :if={is_nil(@installation_type)}>
<div class="text-center mt-8">
Determining installation type...
<.styled_link
:if={@connected?}
href="#"
phx-click="switch-installation-type"
phx-value-method="manual"
>
Skip
</.styled_link>
</div>
</:subtitle>
<:subtitle :if={@flow == PlausibleWeb.Flows.domain_change()}>
<p class="mb-4">
Your domain has been changed.
<strong>
You must update the Plausible Installation on your site within 72 hours to guarantee continuous tracking.
</strong>
<br />
<br /> If you're using the API, please also make sure to update your API credentials.
</p>
</:subtitle>
<:subtitle :if={@flow == PlausibleWeb.Flows.review() and not is_nil(@installation_type)}>
<p class="mb-4">
Review your existing installation. You can skip this step and proceed to verifying your installation.
</p>
</:subtitle>
<:subtitle :if={@installation_type == "WordPress"}>
We've detected your website is using WordPress. Here's how to integrate Plausible:
<.focus_list>
<:item>
<.styled_link href="https://plausible.io/wordpress-analytics-plugin" new_tab={true}>
Install our WordPress plugin
</.styled_link>
</:item>
<:item>
After activating our plugin, click the button below to verify your installation
</:item>
</.focus_list>
</:subtitle>
<:subtitle :if={@installation_type == "GTM"}>
We've detected your website is using Google Tag Manager. Here's how to integrate Plausible:
<.focus_list>
<:item>
<.styled_link href="https://plausible.io/docs/google-tag-manager" new_tab={true}>
Read our Tag Manager guide
</.styled_link>
</:item>
<:item>
Paste this snippet into GTM's Custom HTML section. Once done, click the button below to verify your installation.
</:item>
</.focus_list>
</:subtitle>
<:subtitle :if={@installation_type == "manual"}>
Paste this snippet into the <code>&lt;head&gt;</code>
section of your site. See our
<.styled_link href="https://plausible.io/docs/integration-guides" new_tab={true}>
installation guides.
</.styled_link>
Once done, click the button below to verify your installation.
</:subtitle>
<div :if={@installation_type in ["manual", "GTM"]}>
<.snippet_form
installation_type={@installation_type}
script_config={@script_config}
domain={@domain}
/>
</div>
<.button_link
:if={not is_nil(@installation_type)}
href={"/#{URI.encode_www_form(@domain)}/verification?#{URI.encode_query(@uri_params)}"}
type="submit"
class="w-full mt-8"
>
<%= if @flow == PlausibleWeb.Flows.domain_change() do %>
I understand, I'll update my website
<% else %>
<%= if @flow == PlausibleWeb.Flows.review() do %>
Verify your installation
<% else %>
Start collecting data
<% end %>
<% end %>
</.button_link>
<:footer :if={@initial_installation_type == "WordPress" and @installation_type == "manual"}>
<.styled_link href={} phx-click="switch-installation-type" phx-value-method="WordPress">
Click here
</.styled_link>
if you prefer WordPress installation method.
</:footer>
<:footer :if={@initial_installation_type == "GTM" and @installation_type == "manual"}>
<.styled_link href={} phx-click="switch-installation-type" phx-value-method="GTM">
Click here
</.styled_link>
if you prefer Google Tag Manager installation method.
</:footer>
<:footer :if={not is_nil(@installation_type) and @installation_type != "manual"}>
<.styled_link href={} phx-click="switch-installation-type" phx-value-method="manual">
Click here
</.styled_link>
if you prefer manual installation method.
</:footer>
</PlausibleWeb.Components.Generic.focus_box>
</div>
"""
end
defp render_snippet("manual", domain, %{"404" => true} = script_config) do
script_config = Map.put(script_config, "404", false)
"""
#{render_snippet("manual", domain, script_config)}
#{render_snippet_404()}
"""
end
defp render_snippet("manual", domain, script_config) do
~s|<script defer data-domain="#{domain}" src="#{tracker_url(script_config)}"></script>|
end
defp render_snippet("GTM", domain, %{"404" => true} = script_config) do
script_config = Map.put(script_config, "404", false)
"""
#{render_snippet("GTM", domain, script_config)}
#{render_snippet_404("GTM")}
"""
end
defp render_snippet("GTM", domain, script_config) do
"""
<script>
var script = document.createElement('script');
script.defer = true;
script.dataset.domain = "#{domain}";
script.dataset.api = "https://plausible.io/api/event";
script.src = "#{tracker_url(script_config)}";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
"""
end
def render_snippet_404() do
"<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>"
end
def render_snippet_404("GTM") do
render_snippet_404()
end
defp script_extension_control(assigns) do
~H"""
<div class="mt-2 p-1">
<div class="flex items-center">
<input
type="checkbox"
id={"check-#{@variant}"}
name={@variant}
checked={@config[@variant]}
class="block h-5 w-5 rounded dark:bg-gray-700 border-gray-300 text-indigo-600 focus:ring-indigo-600 mr-2"
/>
<label for={"check-#{@variant}"}>
<%= @label %>
</label>
<div class="ml-2">
<.tooltip sticky?={false} icon?={false} position="z-50 w-64 hidden sm:block">
<:tooltip_content>
<%= @tooltip %>
</:tooltip_content>
<a href={@learn_more} target="_blank" rel="noopener noreferrer">
<Heroicons.information_circle class="text-gray-700 dark:text-gray-500 w-5 h-5" />
</a>
</.tooltip>
</div>
</div>
</div>
"""
end
defp snippet_form(assigns) do
~H"""
<form id="snippet-form" phx-change="update-script-config">
<div class="relative">
<textarea
id="snippet"
class="w-full border-1 border-gray-300 rounded-md p-4 text-gray-700 0 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300"
rows="5"
readonly
><%= render_snippet(@installation_type, @domain, @script_config) %></textarea>
<a
onclick="var input = document.getElementById('snippet'); input.focus(); input.select(); document.execCommand('copy'); event.stopPropagation();"
href="javascript:void(0)"
class="absolute flex items-center text-xs font-medium text-indigo-600 no-underline hover:underline bottom-2 right-4 p-2 bg-white dark:bg-gray-900"
>
<Heroicons.document_duplicate class="pr-1 text-indigo-600 dark:text-indigo-500 w-5 h-5" />
<span>
COPY
</span>
</a>
</div>
<h3 class="text-normal mt-4 font-semibold">Enable optional measurements:</h3>
<.script_extension_control
config={@script_config}
variant="outbound-links"
label="Outbound links"
tooltip="Automatically track clicks on external links"
learn_more="https://plausible.io/docs/outbound-link-click-tracking"
/>
<.script_extension_control
config={@script_config}
variant="file-downloads"
label="File downloads"
tooltip="Automatically track file downloads"
learn_more="https://plausible.io/docs/file-downloads-tracking"
/>
<.script_extension_control
config={@script_config}
variant="404"
label="404 error pages"
tooltip="Find 404 error pages on your site. Additional action required."
learn_more="https://plausible.io/docs/error-pages-tracking-404"
/>
<.script_extension_control
config={@script_config}
variant="hash"
label="Hashed page paths"
tooltip="Automatically track page paths that use a # in the URL"
learn_more="https://plausible.io/docs/hash-based-routing"
/>
<.script_extension_control
config={@script_config}
variant="tagged-events"
label="Custom events"
tooltip="Tag site elements like buttons, links and forms to track user activity. Additional action required."
learn_more="https://plausible.io/docs/custom-event-goals"
/>
<.script_extension_control
config={@script_config}
variant="pageview-props"
label="Custom properties"
tooltip="Attach custom properties (also known as custom dimensions) to pageviews or custom events to create custom metrics. Additional action required."
learn_more="https://plausible.io/docs/custom-props/introduction"
/>
<.script_extension_control
config={@script_config}
variant="revenue"
label="Ecommerce revenue"
tooltip="Assign monetary values to purchases and track revenue attribution. Additional action required."
learn_more="https://plausible.io/docs/ecommerce-revenue-tracking"
/>
</form>
"""
end
def handle_event("switch-installation-type", %{"method" => method}, socket)
when method in @installation_types do
socket = update_uri_params(socket, %{"installation_type" => method})
{:noreply, socket}
end
def handle_event("update-script-config", params, socket) do
new_params =
Enum.into(@script_config_params, %{}, &{&1, params[&1] == "on"})
flash = snippet_change_flash(socket.assigns.script_config, new_params)
socket = update_uri_params(socket, new_params)
{:noreply, put_live_flash(socket, :success, flash)}
end
def handle_params(params, _uri, socket) do
socket = do_handle_params(socket, params)
persist_installation_meta(socket)
{:noreply, socket}
end
defp do_handle_params(socket, params) when is_map(params) do
Enum.reduce(params, socket, &param_reducer/2)
end
defp param_reducer({"installation_type", installation_type}, socket)
when installation_type in @installation_types do
assign(socket,
installation_type: installation_type,
uri_params: Map.put(socket.assigns.uri_params, "installation_type", installation_type)
)
end
defp param_reducer({k, v}, socket)
when k in @script_config_params do
update_script_config(socket, k, v == "true")
end
defp param_reducer(_, socket) do
socket
end
defp update_script_config(socket, "outbound-links" = key, true) do
Plausible.Goals.create_outbound_links(socket.assigns.site)
update_script_config(socket, %{key => true})
end
defp update_script_config(socket, "outbound-links" = key, false) do
Plausible.Goals.delete_outbound_links(socket.assigns.site)
update_script_config(socket, %{key => false})
end
defp update_script_config(socket, "file-downloads" = key, true) do
Plausible.Goals.create_file_downloads(socket.assigns.site)
update_script_config(socket, %{key => true})
end
defp update_script_config(socket, "file-downloads" = key, false) do
Plausible.Goals.delete_file_downloads(socket.assigns.site)
update_script_config(socket, %{key => false})
end
defp update_script_config(socket, "404" = key, true) do
Plausible.Goals.create_404(socket.assigns.site)
update_script_config(socket, %{key => true})
end
defp update_script_config(socket, "404" = key, false) do
Plausible.Goals.delete_404(socket.assigns.site)
update_script_config(socket, %{key => false})
end
defp update_script_config(socket, key, value) do
update_script_config(socket, %{key => value})
end
defp update_script_config(socket, kv) when is_map(kv) do
new_script_config = Map.merge(socket.assigns.script_config, kv)
assign(socket, script_config: new_script_config)
end
defp update_uri_params(socket, params) when is_map(params) do
uri_params = Map.merge(socket.assigns.uri_params, params)
socket
|> assign(uri_params: uri_params)
|> push_patch(
to:
Routes.site_path(
socket,
:installation,
socket.assigns.domain,
uri_params
)
)
end
@domain_change PlausibleWeb.Flows.domain_change()
defp get_installation_type(@domain_change, meta, params) do
meta.installation_type || get_installation_type(nil, nil, params)
end
defp get_installation_type(_site, _meta, params) do
Enum.find(@installation_types, &(&1 == params["installation_type"]))
end
defp tracker_url(script_config) do
extensions = Enum.filter(script_config, fn {_, value} -> value end)
tracker =
["script" | Enum.map(extensions, fn {key, _} -> key end)]
|> Enum.join(".")
"#{PlausibleWeb.Endpoint.url()}/js/#{tracker}.js"
end
defp persist_installation_meta(socket) do
Plausible.Sites.update_installation_meta!(
socket.assigns.site,
%{
installation_type: socket.assigns.installation_type,
script_config: socket.assigns.script_config
}
)
end
defp snippet_change_flash(old_config, new_config) do
change =
Enum.find(new_config, fn {key, value} ->
if old_config[key] != new_config[key] do
{key, value}
end
end)
case change do
{k, false} when k in ["outbound-links", "file-downloads", "404"] ->
"Snippet updated and goal deleted. Please insert the newest snippet into your site"
{_, _} ->
"Snippet updated. Please insert the newest snippet into your site"
end
end
end

View File

@ -6,6 +6,7 @@ defmodule PlausibleWeb.Live.RegisterForm do
use PlausibleWeb, :live_view use PlausibleWeb, :live_view
use Phoenix.HTML use Phoenix.HTML
import PlausibleWeb.Live.Components.Form import PlausibleWeb.Live.Components.Form
import PlausibleWeb.Components.Generic
alias Plausible.Auth alias Plausible.Auth
alias Plausible.Repo alias Plausible.Repo
@ -78,12 +79,12 @@ defmodule PlausibleWeb.Live.RegisterForm do
<PlausibleWeb.Components.FlowProgress.render <PlausibleWeb.Components.FlowProgress.render
:if={@live_action == :register_form} :if={@live_action == :register_form}
flow="register" flow={PlausibleWeb.Flows.register()}
current_step="Register" current_step="Register"
/> />
<PlausibleWeb.Components.FlowProgress.render <PlausibleWeb.Components.FlowProgress.render
:if={@live_action == :register_from_invitation_form} :if={@live_action == :register_from_invitation_form}
flow="invitation" flow={PlausibleWeb.Flows.invitation()}
current_step="Register" current_step="Register"
/> />
@ -188,10 +189,10 @@ defmodule PlausibleWeb.Live.RegisterForm do
</PlausibleWeb.Components.Generic.button> </PlausibleWeb.Components.Generic.button>
<p class="text-center text-gray-600 dark:text-gray-500 mt-4"> <p class="text-center text-gray-600 dark:text-gray-500 mt-4">
Already have an account? <%= link("Log in", Already have an account?
to: "/login", <.styled_link href="/login">
class: "underline text-gray-800 dark:text-gray-50" Log in
) %> </.styled_link>
</p> </p>
</.form> </.form>
</PlausibleWeb.Components.Generic.focus_box> </PlausibleWeb.Components.Generic.focus_box>

View File

@ -71,7 +71,7 @@ defmodule PlausibleWeb.Live.Sites do
You don't have any sites yet. You don't have any sites yet.
</p> </p>
<div class="mt-4 flex sm:ml-4 sm:mt-0"> <div class="mt-4 flex sm:ml-4 sm:mt-0">
<a href="/sites/new?flow=provisioning" class="button"> <a href="/sites/new?flow=#{PlausibleWeb.Flows.provisioning()}" class="button">
+ Add Website + Add Website
</a> </a>
</div> </div>

View File

@ -8,75 +8,59 @@ defmodule PlausibleWeb.Live.Verification do
use Phoenix.HTML use Phoenix.HTML
alias Plausible.Verification.{Checks, State} alias Plausible.Verification.{Checks, State}
alias PlausibleWeb.Live.Components.Modal
@component PlausibleWeb.Live.Components.Verification @component PlausibleWeb.Live.Components.Verification
@slowdown_for_frequent_checking :timer.seconds(5) @slowdown_for_frequent_checking :timer.seconds(5)
def mount( def mount(
:not_mounted_at_router, %{"website" => domain} = params,
%{"domain" => domain} = session, _session,
socket socket
) do ) do
site =
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
:owner,
:admin,
:super_admin,
:viewer
])
private = Map.get(socket.private.connect_info, :private, %{})
socket = socket =
assign(socket, assign(socket,
site: site,
domain: domain, domain: domain,
modal?: !!session["modal?"], has_pageviews?: has_pageviews?(site),
component: @component, component: @component,
report_to: session["report_to"] || self(), installation_type: params["installation_type"],
delay: session["slowdown"] || 500, report_to: self(),
slowdown: session["slowdown"] || 500, delay: private[:delay] || 500,
flow: session["flow"] || "", slowdown: private[:slowdown] || 500,
flow: params["flow"] || "",
checks_pid: nil, checks_pid: nil,
attempts: 0 attempts: 0
) )
if connected?(socket) and !session["modal?"] do if connected?(socket) do
launch_delayed(socket) launch_delayed(socket)
end end
socket =
if connected?(socket) and !!session["modal?"] and !!session["open_modal?"] do
launch_delayed(socket)
Modal.open(socket, "verification-modal")
else
socket
end
{:ok, socket} {:ok, socket}
end end
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div :if={@modal?} phx-click-away="reset"> <PlausibleWeb.Components.FlowProgress.render flow={@flow} current_step="Verify installation" />
<.live_component module={Modal} id="verification-modal">
<.live_component
module={@component}
domain={@domain}
id="verification-within-modal"
modal?={@modal?}
attempts={@attempts}
/>
</.live_component>
<PlausibleWeb.Components.Generic.button
id="launch-verification-button"
x-data
x-on:click={Modal.JS.open("verification-modal")}
phx-click="launch-verification"
class="mt-6"
>
Verify your integration
</PlausibleWeb.Components.Generic.button>
</div>
<.live_component <.live_component
:if={!@modal?}
module={@component} module={@component}
installation_type={@installation_type}
domain={@domain} domain={@domain}
id="verification-standalone" id="verification-standalone"
attempts={@attempts} attempts={@attempts}
flow={@flow} flow={@flow}
awaiting_first_pageview?={not @has_pageviews?}
/> />
""" """
end end
@ -127,6 +111,10 @@ defmodule PlausibleWeb.Live.Verification do
def handle_info({:verification_end, %State{} = state}, socket) do def handle_info({:verification_end, %State{} = state}, socket) do
interpretation = Checks.interpret_diagnostics(state) interpretation = Checks.interpret_diagnostics(state)
if not socket.assigns.has_pageviews? do
Process.send_after(self(), :check_pageviews, socket.assigns.delay * 2)
end
update_component(socket, update_component(socket,
finished?: true, finished?: true,
success?: interpretation.ok?, success?: interpretation.ok?,
@ -136,6 +124,20 @@ defmodule PlausibleWeb.Live.Verification do
{:noreply, assign(socket, checks_pid: nil)} {:noreply, assign(socket, checks_pid: nil)}
end end
def handle_info(:check_pageviews, socket) do
socket =
if has_pageviews?(socket.assigns.site) do
redirect(socket,
external: Routes.stats_url(PlausibleWeb.Endpoint, :stats, socket.assigns.domain, [])
)
else
Process.send_after(self(), :check_pageviews, socket.assigns.delay * 2)
socket
end
{:noreply, socket}
end
defp reset_component(socket) do defp reset_component(socket) do
update_component(socket, update_component(socket,
message: "We're visiting your site to ensure that everything is working", message: "We're visiting your site to ensure that everything is working",
@ -147,20 +149,18 @@ defmodule PlausibleWeb.Live.Verification do
socket socket
end end
defp update_component(socket, updates) do defp update_component(_socket, updates) do
send_update( send_update(
@component, @component,
Keyword.merge(updates, Keyword.merge(updates, id: "verification-standalone")
id:
if(socket.assigns.modal?,
do: "verification-within-modal",
else: "verification-standalone"
)
)
) )
end end
defp launch_delayed(socket) do defp launch_delayed(socket) do
Process.send_after(self(), {:start, socket.assigns.report_to}, socket.assigns.delay) Process.send_after(self(), {:start, socket.assigns.report_to}, socket.assigns.delay)
end end
defp has_pageviews?(site) do
Plausible.Stats.Clickhouse.has_pageviews?(site)
end
end end

View File

@ -26,10 +26,6 @@ defmodule PlausibleWeb.Router do
plug :protect_from_forgery plug :protect_from_forgery
end end
pipeline :focus_layout do
plug :put_root_layout, html: {PlausibleWeb.LayoutView, :focus}
end
pipeline :app_layout do pipeline :app_layout do
plug :put_root_layout, html: {PlausibleWeb.LayoutView, :app} plug :put_root_layout, html: {PlausibleWeb.LayoutView, :app}
end end
@ -227,7 +223,6 @@ defmodule PlausibleWeb.Router do
post "/paddle/webhook", Api.PaddleController, :webhook post "/paddle/webhook", Api.PaddleController, :webhook
get "/paddle/currency", Api.PaddleController, :currency get "/paddle/currency", Api.PaddleController, :currency
get "/:domain/status", Api.InternalController, :domain_status
put "/:domain/disable-feature", Api.InternalController, :disable_feature put "/:domain/disable-feature", Api.InternalController, :disable_feature
get "/sites", Api.InternalController, :sites get "/sites", Api.InternalController, :sites
@ -237,7 +232,7 @@ defmodule PlausibleWeb.Router do
pipe_through [:browser, :csrf] pipe_through [:browser, :csrf]
scope alias: Live, assigns: %{connect_live_socket: true} do scope alias: Live, assigns: %{connect_live_socket: true} do
pipe_through [PlausibleWeb.RequireLoggedOutPlug, :focus_layout] pipe_through [PlausibleWeb.RequireLoggedOutPlug, :app_layout]
scope assigns: %{disable_registration_for: [:invite_only, true]} do scope assigns: %{disable_registration_for: [:invite_only, true]} do
pipe_through PlausibleWeb.Plugs.MaybeDisableRegistration pipe_through PlausibleWeb.Plugs.MaybeDisableRegistration
@ -325,7 +320,6 @@ defmodule PlausibleWeb.Router do
post "/sites", SiteController, :create_site post "/sites", SiteController, :create_site
get "/sites/:website/change-domain", SiteController, :change_domain get "/sites/:website/change-domain", SiteController, :change_domain
put "/sites/:website/change-domain", SiteController, :change_domain_submit put "/sites/:website/change-domain", SiteController, :change_domain_submit
get "/:website/change-domain-snippet", SiteController, :add_snippet_after_domain_change
post "/sites/:website/make-public", SiteController, :make_public post "/sites/:website/make-public", SiteController, :make_public
post "/sites/:website/make-private", SiteController, :make_private post "/sites/:website/make-private", SiteController, :make_private
post "/sites/:website/weekly-report/enable", SiteController, :enable_weekly_report post "/sites/:website/weekly-report/enable", SiteController, :enable_weekly_report
@ -391,7 +385,13 @@ defmodule PlausibleWeb.Router do
get "/sites/:website/weekly-report/unsubscribe", UnsubscribeController, :weekly_report get "/sites/:website/weekly-report/unsubscribe", UnsubscribeController, :weekly_report
get "/sites/:website/monthly-report/unsubscribe", UnsubscribeController, :monthly_report get "/sites/:website/monthly-report/unsubscribe", UnsubscribeController, :monthly_report
get "/:website/snippet", SiteController, :add_snippet scope alias: Live, assigns: %{connect_live_socket: true} do
pipe_through [:app_layout, PlausibleWeb.RequireAccountPlug]
live "/:website/installation", Installation, :installation, as: :site
live "/:website/verification", Verification, :verification, as: :site
end
get "/:website/settings", SiteController, :settings get "/:website/settings", SiteController, :settings
get "/:website/settings/general", SiteController, :settings_general get "/:website/settings/general", SiteController, :settings_general
get "/:website/settings/people", SiteController, :settings_people get "/:website/settings/people", SiteController, :settings_people

View File

@ -60,54 +60,50 @@
<:footer :if={@has_email_code?}> <:footer :if={@has_email_code?}>
<b>Didn't receive an email?</b> <b>Didn't receive an email?</b>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4"> <.focus_list>
<li>Check your spam folder</li> <:item>
<li> Check your spam folder
<%= link("Send a new code", </:item>
class: "underline text-indigo-600", <:item>
to: "/activate/request-code", <.styled_link href="/activate/request-code" method="post">
method: :post Send a new code
) %> to <%= @conn.assigns[:current_user].email %> </.styled_link>
</li> to <%= @conn.assigns[:current_user].email %>
</:item>
<%= if ee?() do %> <:item :if={ee?()}>
<li> <.styled_link href="https://plausible.io/contact" new_tab={true}>
<a class="underline text-indigo-600" href="https://plausible.io/contact"> Contact us
Contact us </.styled_link>
</a> if the problem persists
if the problem persists </:item>
</li> <:item :if={ce?()}>
<% else %> Ask on our
<li> <.styled_link href="https://github.com/plausible/analytics/discussions" new_tab={true}>
Ask on our <%= link("community-supported forum", community-supported forum
to: "https://github.com/plausible/analytics/discussions", </.styled_link>
class: "text-indigo-600 underline" </:item>
) %> </.focus_list>
</li>
<% end %>
</ol>
<b>Entered the wrong email address?</b> <b>Entered the wrong email address?</b>
<ol class="list-disc space-y-1 ml-4 mt-1"> <.focus_list>
<%= if @has_any_memberships? do %> <:item :if={@has_any_memberships?}>
<li> <.styled_link method="post" href="/settings/email/cancel">
<%= link("Change email back to", Change email back to
class: "underline text-indigo-600", </.styled_link>
to: "/settings/email/cancel", <%= @conn.assigns[:current_user].previous_email %>
method: "post" </:item>
) %> to <%= @conn.assigns[:current_user].previous_email %>
</li> <:item :if={not @has_any_memberships?}>
<% else %> <.styled_link
<li> method="delete"
<%= link("Delete this account", href="/me?redirect=/register"
class: "underline text-indigo-600", data-confim="Deleting your account cannot be reversed. Are you sure?"
to: "/me?redirect=/register", >
method: "delete", Delete this account
data: [confirm: "Deleting your account cannot be reversed. Are you sure?"] </.styled_link>
) %> and start over and start over
</li> </:item>
<% end %> </.focus_list>
</ol>
</:footer> </:footer>
</PlausibleWeb.Components.Generic.focus_box> </PlausibleWeb.Components.Generic.focus_box>

View File

@ -8,15 +8,14 @@
</:subtitle> </:subtitle>
<:footer> <:footer>
<p> <.focus_list>
Changed your mind? <:item>
<a Changed your mind?
href={Routes.auth_path(@conn, :user_settings) <> "#setup-2fa"} <.styled_link href={Routes.auth_path(@conn, :user_settings) <> "#setup-2fa"}>
class="underline text-indigo-600" Go back to Settings
> </.styled_link>
Go back to Settings </:item>
</a> </.focus_list>
</p>
</:footer> </:footer>
<div class="flex flex-col sm:flex-row items-center sm:items-start"> <div class="flex flex-col sm:flex-row items-center sm:items-start">
@ -27,27 +26,17 @@
</div> </div>
<div class="mt-8 sm:ml-4"> <div class="mt-8 sm:ml-4">
<ol> <.focus_list>
<li class="flex items-start"> <:item>
<div class="flex-shrink-0 h-5 w-5 relative flex items-center justify-center">
<div class="h-2 w-2 bg-gray-300 dark:bg-gray-700 rounded-full"></div>
</div>
Open the authenticator application Open the authenticator application
</li> </:item>
<li class="mt-1 flex items-start"> <:item>
<div class="flex-shrink-0 h-5 w-5 relative flex items-center justify-center">
<div class="h-2 w-2 bg-gray-300 dark:bg-gray-700 rounded-full"></div>
</div>
Tap Scan a QR Code Tap Scan a QR Code
</li> </:item>
<li class="mt-1 flex items-start"> <:item>
<div class="flex-shrink-0 h-5 w-5 relative flex items-center justify-center">
<div class="h-2 w-2 bg-gray-300 dark:bg-gray-700 rounded-full"></div>
</div>
Scan this code with your phone camera or paste the code manually Scan this code with your phone camera or paste the code manually
</li> </:item>
</ol> </.focus_list>
<div class="sm:ml-2"> <div class="sm:ml-2">
<PlausibleWeb.Live.Components.Form.input_with_clipboard <PlausibleWeb.Live.Components.Form.input_with_clipboard
id="secret" id="secret"

View File

@ -37,22 +37,23 @@
<% end %> <% end %>
<:footer> <:footer>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4"> <.focus_list>
<%= if Keyword.fetch!(Application.get_env(:plausible, :selfhost),:disable_registration) == false do %> <:item>
<li> <%= if Keyword.fetch!(Application.get_env(:plausible, :selfhost), :disable_registration) == false do %>
Don't have an account? <%= link("Register", Don't have an account
to: "/register", <.styled_link href="/register">
class: "text-gray-800 dark:text-gray-50 underline" Register
) %> instead. </.styled_link>
</li> instead.
<% end %> <% end %>
<li> </:item>
<:item>
Forgot password? Forgot password?
<a href="/password/request-reset" class="underline text-gray-800 dark:text-gray-50"> <.styled_link href="/password/reset-request">
Click here Click here
</a> </.styled_link>
to reset it. to reset it.
</li> </:item>
</ol> </.focus_list>
</:footer> </:footer>
</PlausibleWeb.Components.Generic.focus_box> </PlausibleWeb.Components.Generic.focus_box>

View File

@ -1,3 +1,4 @@
<PlausibleWeb.Components.Generic.focus_box>
<%= form_for @changeset, "/settings/api-keys", [class: "w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mt-8"], fn f -> %> <%= form_for @changeset, "/settings/api-keys", [class: "w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mt-8"], fn f -> %>
<h1 class="text-xl font-black dark:text-gray-100">Create new API key</h1> <h1 class="text-xl font-black dark:text-gray-100">Create new API key</h1>
<div class="my-4"> <div class="my-4">
@ -20,3 +21,4 @@
</div> </div>
<%= submit "Continue", class: "button mt-4 w-full" %> <%= submit "Continue", class: "button mt-4 w-full" %>
<% end %> <% end %>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -0,0 +1,52 @@
<PlausibleWeb.Components.Generic.focus_box>
<:title>Create new API key</:title>
<%= form_for @changeset, "/settings/api-keys", fn f -> %>
<div class="my-4">
<%= label(f, :name, class: "block font-medium text-gray-700 dark:text-gray-300") %>
<div class="mt-1">
<%= text_input(f, :name,
class:
"dark:bg-gray-900 shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full border-gray-300 dark:border-gray-500 dark:text-gray-300 dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500 rounded-md",
placeholder: "Development"
) %>
</div>
<%= error_tag(f, :name) %>
</div>
<div class="my-4">
<%= label(f, :key, class: "block font-medium text-gray-700 dark:text-gray-300") %>
<div class="relative mt-1">
<%= text_input(f, :key,
id: "key-input",
class:
"dark:text-gray-300 shadow-sm bg-gray-50 dark:bg-gray-850 focus:ring-indigo-500 focus:border-indigo-500 block w-full border-gray-300 dark:border-gray-500 rounded-md pr-16",
readonly: "readonly"
) %>
<a
onclick="var textarea = document.getElementById('key-input'); textarea.focus(); textarea.select(); document.execCommand('copy');"
href="javascript:void(0)"
class="absolute flex items-center text-xs font-medium text-indigo-600 no-underline hover:underline"
style="top: 12px; right: 12px;"
>
<svg
class="pr-1 text-indigo-600 dark:text-indigo-500"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>COPY
</a>
<%= error_tag(f, :key) %>
<p class="mt-2 text-gray-500 dark:text-gray-200">
Make sure to store the key in a secure place. Once created, we will not be able to show it again.
</p>
</div>
</div>
<%= submit("Continue", class: "button mt-4 w-full") %>
<% end %>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -8,20 +8,20 @@
</:subtitle> </:subtitle>
<:footer> <:footer>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4"> <.focus_list>
<li> <:item>
Can't access your authenticator application? Can't access your authenticator app?
<.styled_link href={Routes.auth_path(@conn, :verify_2fa_recovery_code_form)}> <.styled_link href={Routes.auth_path(@conn, :verify_2fa_recovery_code_form)}>
Use recovery code Use recovery code
</.styled_link> </.styled_link>
</li> </:item>
<li :if={ee?()}> <:item :if={ee?()}>
Lost your recovery codes? Lost your recovery codes?
<.styled_link href="https://plausible.io/contact"> <.styled_link href="https://plausible.io/contact">
Contact us Contact us
</.styled_link> </.styled_link>
</li> </:item>
</ol> </.focus_list>
</:footer> </:footer>
<%= form_for @conn.params, Routes.auth_path(@conn, :verify_2fa), [ <%= form_for @conn.params, Routes.auth_path(@conn, :verify_2fa), [

View File

@ -1,59 +1,57 @@
<div class="w-full max-w-3xl mt-4 mx-auto flex"> <PlausibleWeb.Components.Generic.focus_box>
<:title>
Enter Recovery Code
</:title>
<:subtitle>
Can't access your authenticator application? Enter a recovery code instead.
</:subtitle>
<:footer>
Authenticator application working again?
<a href={Routes.auth_path(@conn, :verify_2fa)} class="underline text-indigo-600">
Enter code
</a>
<%= if ee?() do %>
<br /> Lost your recovery codes?
<a href="https://plausible.io/contact" class="underline text-indigo-600">
Contact us
</a>
<% end %>
</:footer>
<%= form_for @conn.params, <%= form_for @conn.params,
Routes.auth_path(@conn, :verify_2fa_recovery_code), Routes.auth_path(@conn, :verify_2fa_recovery_code),
[ [
class: "w-full max-w-lg mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mb-4 mt-8",
onsubmit: "document.getElementById('use-code-button').disabled = true" onsubmit: "document.getElementById('use-code-button').disabled = true"
], fn f -> %> ], fn f -> %>
<h2 class="text-xl font-black dark:text-gray-100"> <div class="mt-6">
Enter Recovery Code <div>
</h2> <%= text_input(f, :recovery_code,
value: "",
<div class="text-sm mt-2 text-gray-500 dark:text-gray-200 leading-tight"> autocomplete: "off",
Can't access your authenticator application? Enter a recovery code instead. class:
<div class="mt-6"> "font-medium shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full px-2 border-gray-300 dark:border-gray-500 dark:text-gray-200 dark:bg-gray-900 rounded-md",
<div> maxlength: "10",
<%= text_input(f, :recovery_code, oninvalid: "document.getElementById('use-code-button').disabled = false",
value: "", placeholder: "Enter recovery code",
autocomplete: "off", required: "required"
class: ) %>
"font-medium shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full px-2 border-gray-300 dark:border-gray-500 dark:text-gray-200 dark:bg-gray-900 rounded-md",
maxlength: "10",
oninvalid: "document.getElementById('use-code-button').disabled = false",
placeholder: "Enter recovery code",
required: "required"
) %>
</div>
<.button
id="use-code-button"
type="submit"
class="w-full mt-2 [&>span.label-enabled]:block [&>span.label-disabled]:hidden [&[disabled]>span.label-enabled]:hidden [&[disabled]>span.label-disabled]:block"
>
<span class="label-enabled pointer-events-none">
Use Code
</span>
<span class="label-disabled">
<PlausibleWeb.Components.Generic.spinner class="inline-block h-5 w-5 mr-2 text-white dark:text-gray-400" />
Verifying...
</span>
</.button>
</div> </div>
<.button
id="use-code-button"
type="submit"
class="w-full mt-4 [&>span.label-enabled]:block [&>span.label-disabled]:hidden [&[disabled]>span.label-enabled]:hidden [&[disabled]>span.label-disabled]:block"
>
<span class="label-enabled pointer-events-none">
Use Code
</span>
<div class="mt-6 flex flex-row justify-between items-center"> <span class="label-disabled">
<p class="text-sm"> <PlausibleWeb.Components.Generic.spinner class="inline-block h-5 w-5 mr-2 text-white dark:text-gray-400" />
Authenticator application working again? Verifying...
<a href={Routes.auth_path(@conn, :verify_2fa)} class="underline text-indigo-600"> </span>
Enter verification code </.button>
</a>
<%= if ee?() do %>
<br /> Lost your recovery codes?
<a href="https://plausible.io/contact" class="underline text-indigo-600">
Contact us
</a>
<% end %>
</p>
</div>
</div> </div>
<% end %> <% end %>
</div> </PlausibleWeb.Components.Generic.focus_box>

View File

@ -8,24 +8,20 @@
</:subtitle> </:subtitle>
<:footer> <:footer>
<p> <.focus_list>
Changed your mind? <:item>
<a Changed your mind?
href={Routes.auth_path(@conn, :user_settings) <> "#setup-2fa"} <.styled_link href={Routes.auth_path(@conn, :user_settings) <> "#setup-2fa"}>
class="underline text-indigo-600" Go back to Settings
> </.styled_link>
Go back to Settings </:item>
</a> <:item>
</p>
<p>
<%= form_for @conn.params, Routes.auth_path(@conn, :initiate_2fa_setup), [id: "start-over-form"], fn _f -> %>
Having trouble? Having trouble?
<button class="underline text-indigo-600"> <.styled_link method="post" href={Routes.auth_path(@conn, :initiate_2fa_setup)}>
Start over Start over
</button> </.styled_link>
<% end %> </:item>
</p> </.focus_list>
</:footer> </:footer>
<%= form_for @conn.params, Routes.auth_path(@conn, :verify_2fa_setup), [ <%= form_for @conn.params, Routes.auth_path(@conn, :verify_2fa_setup), [

View File

@ -1,82 +0,0 @@
<div class="mx-auto mt-6 text-center">
<h1 class="text-3xl font-black dark:text-gray-100">Confirm new subscription plan</h1>
</div>
<div class="w-full max-w-lg px-4 mt-4 mx-auto">
<div class="flex-1 bg-white dark:bg-gray-800 shadow-md rounded px-8 py-4 mb-4 mt-8">
<div class="text-lg font-bold dark:text-gray-100">Due now</div>
<div class="block text-gray-500 dark:text-gray-200 text-sm">
Your card will be charged a pro-rated amount for the current billing period
</div>
<div class="flex flex-col mt-4">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200 dark:border-t dark:border-l dark:border-r dark:shadow-none">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 dark:bg-gray-900 text-left text-xs leading-4 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider">
Amount
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 dark:bg-gray-900 text-left text-xs leading-4 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider">
Date
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800">
<tr class="border-b border-gray-200">
<td class="px-6 py-4 text-sm leading-5 font-bold dark:text-gray-100"><%= present_currency(@preview_info["immediate_payment"]["currency"]) %><%= @preview_info["immediate_payment"]["amount"] %></td>
<td class="px-6 py-4 text-sm leading-5 dark:text-gray-100"><%= present_date(@preview_info["immediate_payment"]["date"]) %></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pt-6"></div>
<div class="py-4 dark:text-gray-100 text-lg font-bold">Next payment</div>
<div class="flex flex-col">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200 dark:border-t dark:border-l dark:border-r dark:shadow-none">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 dark:bg-gray-900 text-left text-xs leading-4 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider">
Amount
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 dark:bg-gray-900 text-left text-xs leading-4 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider">
Date
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800">
<tr class="border-b border-gray-200">
<td class="px-6 py-4 text-sm leading-5 font-bold dark:text-gray-100"><%= present_currency(@preview_info["immediate_payment"]["currency"]) %><%= @preview_info["next_payment"]["amount"] %></td>
<td class="px-6 py-4 text-sm leading-5 dark:text-gray-100"><%= present_date(@preview_info["next_payment"]["date"]) %></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="flex items-center justify-between mt-10">
<span class="flex rounded-md shadow-sm">
<a href="<%= @back_link %>" type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:text-gray-500 dark:hover:text-gray-200 focus:outline-none focus:border-blue-300 focus:ring active:text-gray-800 dark:active:text-gray-200 active:bg-gray-50 transition ease-in-out duration-150">
Back
</a>
</span>
<span class="flex space-betwee rounded-md shadow-sm">
<%= button("Confirm plan change", to: Routes.billing_path(@conn, :change_plan, @preview_info["plan_id"]), method: :post, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150") %>
</span>
</div>
</div>
</div>
<div class="text-center mt-8 dark:text-gray-100">
Questions? <%= link("Contact us", to: "https://plausible.io/contact", class: "text-indigo-500") %>
</div>

View File

@ -0,0 +1,103 @@
<PlausibleWeb.Components.Generic.focus_box>
<:title>
Confirm new subscription plan
</:title>
<div class="text-lg font-bold dark:text-gray-100">Due now</div>
<div class="block text-gray-500 dark:text-gray-200 text-sm">
Your card will be charged a pro-rated amount for the current billing period
</div>
<div class="flex flex-col mt-4">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200 dark:border-t dark:border-l dark:border-r dark:shadow-none">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 dark:bg-gray-900 text-left text-xs leading-4 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider">
Amount
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 dark:bg-gray-900 text-left text-xs leading-4 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider">
Date
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800">
<tr class="border-b border-gray-200">
<td class="px-6 py-4 text-sm leading-5 font-bold dark:text-gray-100">
<%= present_currency(@preview_info["immediate_payment"]["currency"]) %><%= @preview_info[
"immediate_payment"
]["amount"] %>
</td>
<td class="px-6 py-4 text-sm leading-5 dark:text-gray-100">
<%= present_date(@preview_info["immediate_payment"]["date"]) %>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="pt-6"></div>
<div class="py-4 dark:text-gray-100 text-lg font-bold">Next payment</div>
<div class="flex flex-col">
<div class="-my-2 py-2 overflow-x-auto sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="align-middle inline-block min-w-full shadow overflow-hidden sm:rounded-lg border-b border-gray-200 dark:border-t dark:border-l dark:border-r dark:shadow-none">
<table class="min-w-full">
<thead>
<tr>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 dark:bg-gray-900 text-left text-xs leading-4 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider">
Amount
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-100 dark:bg-gray-900 text-left text-xs leading-4 font-medium text-gray-500 dark:text-gray-200 uppercase tracking-wider">
Date
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800">
<tr class="border-b border-gray-200">
<td class="px-6 py-4 text-sm leading-5 font-bold dark:text-gray-100">
<%= present_currency(@preview_info["immediate_payment"]["currency"]) %><%= @preview_info[
"next_payment"
]["amount"] %>
</td>
<td class="px-6 py-4 text-sm leading-5 dark:text-gray-100">
<%= present_date(@preview_info["next_payment"]["date"]) %>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="flex items-center justify-between mt-10">
<span class="flex rounded-md shadow-sm">
<a
href={@back_link}
type="button"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:text-gray-500 dark:hover:text-gray-200 focus:outline-none focus:border-blue-300 focus:ring active:text-gray-800 dark:active:text-gray-200 active:bg-gray-50 transition ease-in-out duration-150"
>
Back
</a>
</span>
<span class="flex space-betwee rounded-md shadow-sm">
<%= button("Confirm plan change",
to: Routes.billing_path(@conn, :change_plan, @preview_info["plan_id"]),
method: :post,
class:
"inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150"
) %>
</span>
</div>
<div class="text-center mt-8 dark:text-gray-100">
Questions? <%= link("Contact us",
to: "https://plausible.io/contact",
class: "text-indigo-500"
) %>
</div>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -1,51 +0,0 @@
<div class="w-full max-w-lg px-4 mt-4 mx-auto">
<div class="flex-1 bg-white dark:bg-gray-800 shadow-md rounded px-8 py-4 mb-4 mt-8">
<div class="w-full pt-2 text-xl font-bold dark:text-gray-100">
Your account is being upgraded...
</div>
<p class="text-gray-500 dark:text-gray-200 text-sm py-4">
Thank you for upgrading your subscription! We're still working on
upgrading your account, and you'll be automatically redirected in a few
seconds.
</p>
<p hidden id="timeout-notice" class="text-gray-500 dark:text-gray-200 text-sm">
Your subscription is taking longer than usual to upgrade. If you're not
redirected soon, please contact <a class="text-indigo-500" href="mailto:hello@plausible.io">hello@plausible.io</a>.
</p>
<div class="loading my-12 mx-auto"><div></div></div>
<script>
const PING_SUBSCRIPTION_API = "<%= Routes.billing_path(@conn, :ping_subscription) %>"
const REDIRECT_TO = "<%= Routes.auth_path(@conn, :user_settings) %>"
const PING_EVERY_MS = 2000
const TIMEOUT_AFTER_MS = 15000
const ping = async function(fun) {
let result = {}
while (!result.is_subscribed) {
await wait();
const response = await fetch(PING_SUBSCRIPTION_API)
result = await response.json()
}
window.location = REDIRECT_TO
}
const wait = function() {
return new Promise(resolve => { setTimeout(resolve, PING_EVERY_MS) })
}
setTimeout(() => {
document.getElementById("timeout-notice").removeAttribute("hidden")
}, TIMEOUT_AFTER_MS)
// Pings pingSubscriptionUrl every 2 seconds until an active subscription
// is created from Paddle webhooks.
ping()
</script>
</div>
</div>

View File

@ -0,0 +1,51 @@
<PlausibleWeb.Components.Generic.focus_box>
<:title>
Your account is being upgraded...
</:title>
<:subtitle>
Thank you for upgrading your subscription! We're still working on
upgrading your account, and you'll be automatically redirected in a few
seconds.
</:subtitle>
<p hidden id="timeout-notice" class="text-gray-500 dark:text-gray-200 text-sm">
Your subscription is taking longer than usual to upgrade. If you're not
redirected soon, please contact <a class="text-indigo-500" href="mailto:hello@plausible.io">hello@plausible.io</a>.
</p>
<div class="loading my-12 mx-auto">
<div></div>
</div>
<script>
const PING_SUBSCRIPTION_API = "<%= Routes.billing_path(@conn, :ping_subscription) %>"
const REDIRECT_TO = "<%= Routes.auth_path(@conn, :user_settings) %>"
const PING_EVERY_MS = 2000
const TIMEOUT_AFTER_MS = 15000
const ping = async function(fun) {
let result = {}
while (!result.is_subscribed) {
await wait();
const response = await fetch(PING_SUBSCRIPTION_API)
result = await response.json()
}
window.location = REDIRECT_TO
}
const wait = function() {
return new Promise(resolve => { setTimeout(resolve, PING_EVERY_MS) })
}
setTimeout(() => {
document.getElementById("timeout-notice").removeAttribute("hidden")
}, TIMEOUT_AFTER_MS)
// Pings pingSubscriptionUrl every 2 seconds until an active subscription
// is created from Paddle webhooks.
ping()
</script>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -2,7 +2,7 @@
<section class="grid grid-cols-1 gap-y-3 divide-y"> <section class="grid grid-cols-1 gap-y-3 divide-y">
<%= for log <- @queries do %> <%= for log <- @queries do %>
<details class="group py-1"> <details class="group py-1">
<summary class="flex cursor-pointer flex-row items-center justify-between py-1 font-semibold text-gray-600 dark:text-gray-200 pt-4"> <summary class="flex cursor-pointer flex-row items-center justify-between py-1 font-semibold text-gray-800 dark:text-gray-200 pt-4">
<%= log["request_method"] %> <%= controller_name(log["phoenix_controller"]) %>.<%= log[ <%= log["request_method"] %> <%= controller_name(log["phoenix_controller"]) %>.<%= log[
"phoenix_action" "phoenix_action"
] %> (<%= log[:query_duration_ms] %>ms) ] %> (<%= log[:query_duration_ms] %>ms)
@ -18,7 +18,7 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path> <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
</svg> </svg>
</summary> </summary>
<table class="table table-striped table-auto text-gray-400 dark:text-gray-200"> <table class="table table-striped table-auto text-gray-800 dark:text-gray-200">
<tbody> <tbody>
<%= for {key, value} <- log do %> <%= for {key, value} <- log do %>
<tr class="table-row"> <tr class="table-row">

View File

@ -4,5 +4,5 @@ We've recorded <%= @current_visitors %> visitors on <%= link(@site.domain, to: "
<br /><br /> <br /><br />
View dashboard: <%= link(@dashboard_link, to: @dashboard_link) %> View dashboard: <%= link(@dashboard_link, to: @dashboard_link) %>
<br /><br /> <br /><br />
Something looks off? Please use our <%= link("integration testing tool", to: @settings_link) %> to verify that Plausible has been integrated correctly. Something looks off? Please <%= link("review your installation", to: @installation_link) %> to verify that Plausible has been integrated correctly.
<% end %> <% end %>

View File

@ -2,7 +2,7 @@
You signed up for a free 30-day trial of Plausible, a simple and privacy-friendly website analytics tool. You signed up for a free 30-day trial of Plausible, a simple and privacy-friendly website analytics tool.
<br /><br /> <br /><br />
<% end %> <% end %>
To finish your setup for <%= @site.domain %>, you need to install <%= link("this lightweight line of JavaScript code", to: "#{plausible_url()}/#{URI.encode_www_form(@site.domain)}/snippet") %> into your site to start collecting visitor statistics. To finish your setup for <%= @site.domain %>, review <%= link("your installation", to: "#{plausible_url()}/#{URI.encode_www_form(@site.domain)}/installation") %> and start collecting visitor statistics.
<br /><br /> <br /><br />
This Plausible script is 45 times smaller than Google Analytics script so youll have a fast loading site while getting all the important traffic insights on one single page. This Plausible script is 45 times smaller than Google Analytics script so youll have a fast loading site while getting all the important traffic insights on one single page.
<br /><br /> <br /><br />

View File

@ -1,42 +1,45 @@
<%= form_for @conn, Routes.google_analytics_path(@conn, :property, @site.domain), [onsubmit: "continueButton.disabled = true; return true;", class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> <PlausibleWeb.Components.Generic.focus_box>
<h2 class="text-xl font-black dark:text-gray-100">Import from Google Analytics</h2> <:title>
Import from Google Analytics
</:title>
<%= hidden_input(f, :access_token, value: @access_token) %> <:subtitle>
<%= hidden_input(f, :refresh_token, value: @refresh_token) %>
<%= hidden_input(f, :expires_at, value: @expires_at) %>
<div class="mt-6 text-sm text-gray-500 dark:text-gray-200">
Choose the property in your Google Analytics account that will be imported to the <%= @site.domain %> dashboard. Choose the property in your Google Analytics account that will be imported to the <%= @site.domain %> dashboard.
</div> </:subtitle>
<%= form_for @conn, Routes.google_analytics_path(@conn, :property, @site.domain), [onsubmit: "continueButton.disabled = true; return true;"], fn f -> %>
<%= hidden_input(f, :access_token, value: @access_token) %>
<%= hidden_input(f, :refresh_token, value: @refresh_token) %>
<%= hidden_input(f, :expires_at, value: @expires_at) %>
<div class="mt-3"> <div class="mt-3">
<%= styled_label(f, :property, "Google Analytics property") %> <%= styled_label(f, :property, "Google Analytics property") %>
<%= styled_select(f, :property, @properties, <%= styled_select(f, :property, @properties,
prompt: "(Choose property)", prompt: "(Choose property)",
required: "true" required: "true"
) %> ) %>
<%= styled_error(@conn.assigns[:selected_property_error]) %> <%= styled_error(@conn.assigns[:selected_property_error]) %>
</div> </div>
<div class="mt-6 flex flex-col-reverse sm:flex-row justify-between items-center"> <div class="mt-6 flex flex-col-reverse sm:flex-row justify-between items-center">
<p class="text-sm mt-4 sm:mt-0 dark:text-gray-100"> <p class="mt-4 sm:mt-0 dark:text-gray-100">
<a <a
href={Routes.site_path(@conn, :settings_imports_exports, @site.domain)} href={Routes.site_path(@conn, :settings_imports_exports, @site.domain)}
class="underline text-indigo-600" class="underline text-indigo-600"
> >
Go back Go back
</a> </a>
</p> </p>
<%= submit(name: "continueButton", class: "button sm:w-auto w-full [&>span.label-enabled]:block [&>span.label-disabled]:hidden [&[disabled]>span.label-enabled]:hidden [&[disabled]>span.label-disabled]:block") do %> <%= submit(name: "continueButton", class: "button sm:w-auto w-full [&>span.label-enabled]:block [&>span.label-disabled]:hidden [&[disabled]>span.label-enabled]:hidden [&[disabled]>span.label-disabled]:block") do %>
<span class="label-enabled pointer-events-none"> <span class="label-enabled pointer-events-none">
Continue -> Continue
</span> </span>
<span class="label-disabled"> <span class="label-disabled">
<PlausibleWeb.Components.Generic.spinner class="inline-block h-5 w-5 mr-2 text-white dark:text-gray-400" /> <PlausibleWeb.Components.Generic.spinner class="inline-block h-5 w-5 mr-2 text-white dark:text-gray-400" />
Checking... Checking...
</span> </span>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -1,67 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="A lightweight, non-intrusive alternative to Google Analytics."
/>
<meta name="robots" content={@conn.private.robots} />
<%= if assigns[:connect_live_socket] do %>
<meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} />
<meta name="websocket-url" content={websocket_url()} />
<% end %>
<PlausibleWeb.Components.Layout.favicon conn={@conn} />
<title><%= assigns[:title] || "Plausible · Web analytics" %></title>
<link rel="stylesheet" href={Routes.static_path(@conn, "/css/app.css")} />
<%= render("_tracking.html", assigns) %>
</head>
<body class="flex flex-col h-full bg-gray-100 dark:bg-gray-900">
<nav class="relative z-20 py-8">
<div class="container print:max-w-full">
<nav class="relative flex items-center justify-between sm:h-10 md:justify-center">
<div class="flex items-center flex-1 md:absolute md:inset-y-0 md:left-0">
<a href={home_dest(@conn)}>
<%= img_tag(
PlausibleWeb.Router.Helpers.static_path(
@conn,
logo_path("logo_dark.svg")
),
class: "w-44 -mt-2 hidden dark:inline",
alt: "Plausible logo",
loading: "lazy"
) %>
<%= img_tag(
PlausibleWeb.Router.Helpers.static_path(
@conn,
logo_path("logo_light.svg")
),
class: "w-44 -mt-2 inline dark:hidden",
alt: "Plausible logo",
loading: "lazy"
) %>
</a>
</div>
</nav>
</div>
</nav>
<%= if assigns[:flash] do %>
<%= render("_flash.html", assigns) %>
<% end %>
<%= @inner_content %>
<%= if ee?() do %>
<p class="text-center text-gray-500 text-xs py-8">
© <%= DateTime.utc_now().year %> Plausible Analytics. All rights reserved.
</p>
<% end %>
<PlausibleWeb.Components.Layout.theme_script current_user={assigns[:current_user]} />
<script type="text/javascript" src={Routes.static_path(@conn, "/js/app.js")}>
</script>
</body>
</html>

View File

@ -14,27 +14,27 @@
</p> </p>
<:footer> <:footer>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4"> <.focus_list>
<li> <:item>
<.styled_link href={Routes.auth_path(@conn, :login)}> <.styled_link href={Routes.auth_path(@conn, :login)}>
Login Login
</.styled_link> </.styled_link>
</li> </:item>
<li> <:item>
<.styled_link href={Routes.auth_path(@conn, :register_form)}> <.styled_link href={Routes.auth_path(@conn, :register_form)}>
Register Register
</.styled_link> </.styled_link>
</li> </:item>
<li> <:item>
<.styled_link href="https://plausible.io/docs"> <.styled_link href="https://plausible.io/docs">
Guides & Docs Guides & Docs
</.styled_link> </.styled_link>
</li> </:item>
<li> <:item>
<.styled_link href="https://twitter.com/plausiblehq"> <.styled_link href="https://twitter.com/plausiblehq">
Follow on Twitter Follow on Twitter
</.styled_link> </.styled_link>
</li> </:item>
</ol> </.focus_list>
</:footer> </:footer>
</PlausibleWeb.Components.Generic.focus_box> </PlausibleWeb.Components.Generic.focus_box>

View File

@ -1,44 +1,50 @@
<div class="w-full max-w-3xl mt-4 mx-auto flex"> <PlausibleWeb.Components.FlowProgress.render
<%= form_for @changeset, Routes.site_path(@conn, :change_domain_submit, @site.domain), [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-lg rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> flow={PlausibleWeb.Flows.domain_change()}
<h2 class="text-xl font-black dark:text-gray-100">Change your website domain</h2> current_step="Set up new domain"
/>
<PlausibleWeb.Components.Generic.focus_box>
<:title>Change your website domain</:title>
<:subtitle>
Once you change your domain, <b>you must update Plausible Installation on your site within 72 hours to guarantee continuous tracking</b>.
<br /><br />If you're using the API, please also make sure to update your API credentials. Visit our
<.styled_link new_tab href="https://plausible.io/docs/change-domain-name/">
documentation
</.styled_link>
for details.
</:subtitle>
<:footer>
<.focus_list>
<:item>
Changed your mind? Go back to
<.styled_link href={Routes.site_path(@conn, :settings_general, @site.domain)}>
Site Settings
</.styled_link>
</:item>
</.focus_list>
</:footer>
<%= form_for @changeset, Routes.site_path(@conn, :change_domain_submit, @site.domain, flow: PlausibleWeb.Flows.domain_change()), [], fn f -> %>
<h2 class="text-xl font-black dark:text-gray-100"></h2>
<div class="my-6"> <div class="my-6">
<%= label(f, :domain, class: "block text-sm font-medium text-gray-700 dark:text-gray-300") %> <%= label(f, :domain, class: "block font-medium dark:text-gray-300") %>
<p class="text-gray-500 dark:text-gray-400 text-xs mt-1"> <p class="text-gray-500 dark:text-gray-400 mt-1 text-sm">
Just the naked domain or subdomain without 'www' Just the naked domain or subdomain without 'www', 'https' etc.
</p> </p>
<div class="mt-2 flex rounded-md shadow-sm"> <div class="mt-2 flex rounded-md shadow-sm">
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-850 text-gray-500 dark:text-gray-400 sm:text-sm">
https://
</span>
<%= text_input(f, :domain, <%= text_input(f, :domain,
class: class:
"focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md sm:text-sm border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300", "focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300",
placeholder: "example.com" placeholder: "example.com"
) %> ) %>
</div> </div>
<%= error_tag(f, :domain) %> <%= error_tag(f, :domain) %>
</div> </div>
<p class="text-sm sm:text-sm text-gray-700 dark:text-gray-300">
<span class="font-bold dark:text-gray-100">Once you change your domain, you must update the JavaScript snippet on your site within 72 hours to guarantee continuous tracking</span>. If you're using the API, please also make sure to update your API credentials.
</p>
<p class="text-sm text-gray-700 dark:text-gray-300 mt-4">
Visit our
<.styled_link new_tab href="https://plausible.io/docs/change-domain-name/">
documentation
</.styled_link>
for details.
</p>
<PlausibleWeb.Components.Generic.button type="submit" class="mt-4 w-full"> <PlausibleWeb.Components.Generic.button type="submit" class="mt-4 w-full">
Change Domain and add new Snippet Change Domain and add new Snippet
</PlausibleWeb.Components.Generic.button> </PlausibleWeb.Components.Generic.button>
<div class="text-center mt-4">
<.styled_link href={Routes.site_path(@conn, :settings_general, @site.domain)}>
Back to Site Settings
</.styled_link>
</div>
<% end %> <% end %>
</div> </PlausibleWeb.Components.Generic.focus_box>

View File

@ -1,121 +1,113 @@
<%= form_for @conn, Routes.membership_path(@conn, :invite_member, @site.domain), [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %> <PlausibleWeb.Components.Generic.focus_box>
<h2 class="text-xl font-black dark:text-gray-100 mb-4">Invite member to <%= @site.domain %></h2> <:title>
Invite member to <%= @site.domain %>
</:title>
<PlausibleWeb.Components.Billing.Notice.limit_exceeded <:subtitle>
:if={Map.get(assigns, :is_at_limit, false)} Enter the email address and role of the person you want to invite. We will contact them over email to offer them access to <%= @site.domain %> analytics.<br /><br />
current_user={@current_user}
billable_user={@site.owner}
limit={Map.get(assigns, :team_member_limit, 0)}
resource="team members"
/>
<p class="mt-4 max-w-2xl text-sm text-gray-500 dark:text-gray-200">
Enter the email address and role of the person you want to invite. We will contact them over email to offer them access to <%= @site.domain %> analytics.
</p>
<p class="mt-4 max-w-2xl text-sm text-gray-500 dark:text-gray-200">
The invitation will expire in 48 hours The invitation will expire in 48 hours
</p> </:subtitle>
<div class="my-6"> <%= form_for @conn, Routes.membership_path(@conn, :invite_member, @site.domain), fn f -> %>
<%= label(f, :email, "Email address", <PlausibleWeb.Components.Billing.Notice.limit_exceeded
class: "block text-sm font-medium text-gray-700 dark:text-gray-300" :if={Map.get(assigns, :is_at_limit, false)}
) %> current_user={@current_user}
<div class="mt-1 relative rounded-md shadow-sm"> billable_user={@site.owner}
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> limit={Map.get(assigns, :team_member_limit, 0)}
<svg resource="team members"
class="h-5 w-5 text-gray-500 dark:text-gray-400" />
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" <div class="my-6">
fill="currentColor" <%= label(f, :email, "Email address",
aria-hidden="true" class: "block font-medium text-gray-700 dark:text-gray-300"
>
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>
</div>
<%= email_input(f, :email,
class:
"focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-full rounded-md pl-10 sm:text-sm border-gray-300 dark:border-gray-500",
placeholder: "john.doe@example.com",
required: "true"
) %> ) %>
</div> <div class="mt-1 relative rounded-md shadow-sm">
<%= error_tag(f, :email) %> <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
<%= if @conn.assigns[:error] do %> class="h-5 w-5 text-gray-500 dark:text-gray-400"
<div class="text-red-500 text-xs mb-4"><%= @conn.assigns[:error] %></div> xmlns="http://www.w3.org/2000/svg"
<% end %> viewBox="0 0 20 20"
</div> fill="currentColor"
aria-hidden="true"
<fieldset x-data="{selectedOption: null}"> >
<%= label(f, :role, class: "block text-sm font-medium text-gray-700 dark:text-gray-300") %> <path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
<div class="mt-1 bg-white rounded-md -space-y-px dark:bg-gray-800"> </svg>
<label </div>
class="border-gray-200 dark:border-gray-500 rounded-tl-md rounded-tr-md relative border p-4 flex cursor-pointer" <%= email_input(f, :email,
x-class="{'bg-indigo-50 border-indigo-200 dark:bg-indigo-500 dark:border-indigo-800 z-10': selectedOption === 'admin', 'border-gray-200': selectedOption !== 'admin'}"
>
<%= radio_button(f, :role, "admin",
class: class:
"dark:bg-gray-900 h-4 w-4 mt-0.5 cursor-pointer text-indigo-600 border-gray-300 dark:border-gray-500 focus:ring-indigo-500", "focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-900 dark:text-gray-300 block w-full rounded-md pl-10 border-gray-300 dark:border-gray-500",
"x-model": "selectedOption", placeholder: "john.doe@example.com",
required: "true" required: "true"
) %> ) %>
<div class="ml-3 flex flex-col"> </div>
<span <%= error_tag(f, :email) %>
class="text-gray-900 dark:text-gray-100 block text-sm font-medium"
x-class="{'text-indigo-900 dark:text-white': selectedOption === 'admin', 'text-gray-900 dark:text-gray-100': selectedOption !== 'admin'}"
>
Admin
</span>
<span
class="text-gray-500 dark:text-gray-200 block text-sm"
x-class="{'text-indigo-700 dark:text-gray-100': selectedOption === 'admin', 'text-gray-500 dark:text-gray-200': selectedOption !== 'admin'}"
>
Can view stats, change site settings and invite other members
</span>
</div>
</label>
<label <%= if @conn.assigns[:error] do %>
class="border-gray-200 dark:border-gray-500 rounded-bl-md rounded-br-md relative border p-4 flex cursor-pointer" <div class="text-red-500 mb-4"><%= @conn.assigns[:error] %></div>
x-class="{'bg-indigo-50 border-indigo-200 dark:bg-indigo-500 dark:border-indigo-800 z-10': selectedOption === 'viewer', 'border-gray-200': selectedOption !== 'viewer'}" <% end %>
>
<%= radio_button(f, :role, "viewer",
class:
"dark:bg-gray-900 h-4 w-4 mt-0.5 cursor-pointer text-indigo-600 border-gray-300 dark:border-gray-500 focus:ring-indigo-500",
"x-model": "selectedOption",
required: "true"
) %>
<div class="ml-3 flex flex-col">
<span
class="text-gray-900 dark:text-gray-100 block text-sm font-medium"
x-class="{'text-indigo-900 dark:text-white': selectedOption === 'viewer', 'text-gray-900 dark:text-gray-100': selectedOption !== 'viewer'}"
>
Viewer
</span>
<span
class="text-gray-500 dark:text-gray-200 block text-sm"
x-class="{'text-indigo-700 dark:text-gray-100': selectedOption === 'viewer', 'text-gray-500 dark:text-gray-200': selectedOption !== 'viewer'}"
>
Can view stats but cannot access settings or invite members
</span>
</div>
</label>
</div> </div>
</fieldset>
<div class="mt-6"> <fieldset x-data="{selectedOption: null}">
<%= submit(class: "button w-full") do %> <%= label(f, :role, class: "block font-medium text-gray-700 dark:text-gray-300") %>
<svg <div class="mt-1 bg-white rounded-md -space-y-px dark:bg-gray-800">
class="w-5 h-5 mr-1" <label
fill="currentColor" class="border-gray-200 dark:border-gray-500 rounded-tl-md rounded-tr-md relative border p-4 flex cursor-pointer"
viewBox="0 0 20 20" x-class="{'bg-indigo-50 border-indigo-200 dark:bg-indigo-500 dark:border-indigo-800 z-10': selectedOption === 'admin', 'border-gray-200': selectedOption !== 'admin'}"
xmlns="http://www.w3.org/2000/svg" >
> <%= radio_button(f, :role, "admin",
<path d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z"> class:
</path> "dark:bg-gray-900 h-4 w-4 mt-0.5 cursor-pointer text-indigo-600 border-gray-300 dark:border-gray-500 focus:ring-indigo-500",
</svg> "x-model": "selectedOption",
<span>Invite</span> required: "true"
<% end %> ) %>
</div> <div class="ml-3 flex flex-col">
<% end %> <span
class="text-gray-900 dark:text-gray-100 block font-medium"
x-class="{'text-indigo-900 dark:text-white': selectedOption === 'admin', 'text-gray-900 dark:text-gray-100': selectedOption !== 'admin'}"
>
Admin
</span>
<span
class="text-gray-500 dark:text-gray-200 block"
x-class="{'text-indigo-700 dark:text-gray-100': selectedOption === 'admin', 'text-gray-500 dark:text-gray-200': selectedOption !== 'admin'}"
>
Can view stats, change site settings and invite other members
</span>
</div>
</label>
<label
class="border-gray-200 dark:border-gray-500 rounded-bl-md rounded-br-md relative border p-4 flex cursor-pointer"
x-class="{'bg-indigo-50 border-indigo-200 dark:bg-indigo-500 dark:border-indigo-800 z-10': selectedOption === 'viewer', 'border-gray-200': selectedOption !== 'viewer'}"
>
<%= radio_button(f, :role, "viewer",
class:
"dark:bg-gray-900 h-4 w-4 mt-0.5 cursor-pointer text-indigo-600 border-gray-300 dark:border-gray-500 focus:ring-indigo-500",
"x-model": "selectedOption",
required: "true"
) %>
<div class="ml-3 flex flex-col">
<span
class="text-gray-900 dark:text-gray-100 block font-medium"
x-class="{'text-indigo-900 dark:text-white': selectedOption === 'viewer', 'text-gray-900 dark:text-gray-100': selectedOption !== 'viewer'}"
>
Viewer
</span>
<span
class="text-gray-500 dark:text-gray-200 block"
x-class="{'text-indigo-700 dark:text-gray-100': selectedOption === 'viewer', 'text-gray-500 dark:text-gray-200': selectedOption !== 'viewer'}"
>
Can view stats but cannot access settings or invite members
</span>
</div>
</label>
</div>
</fieldset>
<div class="mt-6">
<%= submit(class: "button w-full") do %>
<span>Invite</span>
<% end %>
</div>
<% end %>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -1,34 +0,0 @@
<%= form_for @conn, Routes.membership_path(@conn, :transfer_ownership, @site.domain), [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-black dark:text-gray-100">Transfer ownership of <%= @site.domain %></h2>
<p class="mt-4 max-w-2xl text-sm text-gray-500">
Enter the email address of the new owner. We will contact them over email to
offer them the ownership of <%= @site.domain %>. If they don't respond in 48
hours, the request will expire automatically.
</p>
<p class="mt-4 max-w-2xl text-sm text-gray-500">
Do note that a subscription plan is not transferred alongside the site. If
they accept the transfer request, the new owner will need to have an active
subscription. Your access will be downgraded to <b>admin</b> and any other
member roles will stay the same.
</p>
<%= if @conn.assigns[:error] do %>
<div class="text-red-500 text-xs italic mt-4"><%= @conn.assigns[:error] %></div>
<% end %>
<div class="my-6">
<%= label f, :email, "Email address", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<div class="mt-1 relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" /></svg>
</div>
<%= email_input(f, :email, class: "focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-md pl-10 sm:text-sm border-gray-300", placeholder: "john.doe@example.com", required: "true") %>
</div>
<%= error_tag f, :email %>
</div>
<div class="mt-6">
<%= submit("Request transfer", class: "button w-full") %>
</div>
<% end %>

View File

@ -0,0 +1,50 @@
<PlausibleWeb.Components.Generic.focus_box>
<:title>
Transfer ownership of <%= @site.domain %>
</:title>
<:subtitle>
Enter the email address of the new owner. We will contact them over email to
offer them the ownership of <%= @site.domain %>. If they don't respond in 48
hours, the request will expire automatically. <br /><br />
Do note that a subscription plan is not transferred alongside the site. If
they accept the transfer request, the new owner will need to have an active
subscription. Your access will be downgraded to <b>admin</b>
and any other
member roles will stay the same.
</:subtitle>
<%= form_for @conn, Routes.membership_path(@conn, :transfer_ownership, @site.domain), fn f -> %>
<%= if @conn.assigns[:error] do %>
<div class="text-red-500 text-xs italic mt-4"><%= @conn.assigns[:error] %></div>
<% end %>
<div class="my-6">
<%= label(f, :email, "Email address",
class: "block font-medium text-gray-700 dark:text-gray-300"
) %>
<div class="mt-1 relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg
class="h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>
</div>
<%= email_input(f, :email,
class:
"focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-md pl-10 border-gray-300",
placeholder: "john.doe@example.com",
required: "true"
) %>
</div>
<%= error_tag(f, :email) %>
</div>
<div class="mt-6">
<%= submit("Request transfer", class: "button w-full") %>
</div>
<% end %>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -46,12 +46,9 @@
<div class="my-6"> <div class="my-6">
<%= label(f, :domain, class: "block font-medium dark:text-gray-300") %> <%= label(f, :domain, class: "block font-medium dark:text-gray-300") %>
<p class="text-gray-500 dark:text-gray-400 mt-1 text-sm"> <p class="text-gray-500 dark:text-gray-400 mt-1 text-sm">
Just the naked domain or subdomain without 'www' Just the naked domain or subdomain without 'www', 'https' etc.
</p> </p>
<div class="mt-2 flex rounded-md shadow-sm"> <div class="mt-2 flex rounded-md shadow-sm">
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 dark:border-gray-500 bg-gray-50 dark:bg-gray-850 dark:text-gray-400">
https://
</span>
<%= text_input(f, :domain, <%= text_input(f, :domain,
class: class:
"focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300", "focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 flex-1 block w-full px-3 py-2 rounded-none rounded-r-md border-gray-300 dark:border-gray-500 dark:bg-gray-900 dark:text-gray-300",

View File

@ -3,7 +3,7 @@
<header class="relative"> <header class="relative">
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Site Domain</h2> <h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">Site Domain</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200"> <p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Moving your Site to a different Domain? We got you! Moving your site to a different domain? We got you!
</p> </p>
<PlausibleWeb.Components.Generic.docs_info slug="change-domain-name" /> <PlausibleWeb.Components.Generic.docs_info slug="change-domain-name" />
@ -39,7 +39,7 @@
Site Timezone Site Timezone
</h2> </h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200"> <p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Update your reporting Timezone. Update your reporting timezone.
</p> </p>
<PlausibleWeb.Components.Generic.docs_info slug="general" /> <PlausibleWeb.Components.Generic.docs_info slug="general" />
@ -62,63 +62,28 @@
</div> </div>
<% end %> <% end %>
<%= form_for @conn, "/", [class: "shadow bg-white dark:bg-gray-800 sm:rounded-md sm:overflow-hidden py-6 px-4 sm:p-6"], fn f -> %> <div class="shadow sm:rounded-md sm:overflow-hidden">
<header class="relative"> <div class="bg-white dark:bg-gray-800 py-6 px-4 space-y-6 sm:p-6">
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100"> <header class="relative">
<a id="snippet">JavaScript Snippet</a> <h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
</h2> <a id="snippet">Site Installation</a>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200"> </h2>
Include this Snippet in the <code>&lt;head&gt;</code> of your Website. <p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
</p> Control what data is collected and verify your installation.
</p>
<PlausibleWeb.Components.Generic.docs_info slug="plausible-script" /> <PlausibleWeb.Components.Generic.docs_info slug="plausible-script" />
</header> </header>
<div class="my-4"> <div class="my-4">
<div class="relative"> <PlausibleWeb.Components.Generic.button_link
<code> class="mt-4"
<%= textarea(f, :domain, href={
id: "snippet_code", Routes.site_path(@conn, :installation, @site.domain, flow: PlausibleWeb.Flows.review())
class:
"transition overflow-hidden bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white focus:border-gray-300 dark:focus:border-gray-500 text-xs mt-2 resize-none",
value: render_snippet(@site),
rows: 2
) %>
</code>
<a
onclick="var textarea = document.getElementById('snippet_code'); textarea.focus(); textarea.select(); document.execCommand('copy');"
href="javascript:void(0)"
class="no-underline text-indigo-500 text-sm hover:underline"
>
<svg
class="absolute text-indigo-500"
style="top: 24px; right: 12px;"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</a>
</div>
<div :if={Plausible.Verification.enabled?()}>
<%= live_render(@conn, PlausibleWeb.Live.Verification,
session: %{
"open_modal?" => !!@conn.params["launch_verification"],
"site_id" => @site.id,
"domain" => @site.domain,
"modal?" => true,
"slowdown" => @conn.private[:verification_slowdown]
} }
) %> >
Review Installation
</PlausibleWeb.Components.Generic.button_link>
</div> </div>
</div> </div>
<% end %> </div>

View File

@ -1,91 +0,0 @@
<PlausibleWeb.Components.FirstDashboardLaunchBanner.set
:if={@conn.params["site_created"] == "true"}
site={@site}
/>
<PlausibleWeb.Components.FlowProgress.render
flow={@conn.params["flow"]}
current_step="Install snippet"
/>
<PlausibleWeb.Components.Generic.focus_box>
<:title>
Add JavaScript snippet
</:title>
<:subtitle>
<p :if={Plausible.Verification.enabled?()} class="dark:text-gray-100">
Include this snippet in the <code>&lt;head&gt;</code>
section of your website.<br />To verify your integration, click the button below to confirm that everything is working correctly.
</p>
<p :if={not Plausible.Verification.enabled?()} class="dark:text-gray-100">
Paste this snippet in the <code>&lt;head&gt;</code> of your website.
</p>
</:subtitle>
<:footer>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4">
<li>
On WordPress? We have
<.styled_link new_tab href="https://plausible.io/wordpress-analytics-plugin">
a plugin
</.styled_link>
</li>
<li>
See more
<.styled_link new_tab href="https://plausible.io/docs/integration-guides">
integration guides
</.styled_link>
</li>
</ol>
</:footer>
<%= form_for @conn, @form_submit_url, [], fn f -> %>
<div>
<div class="relative">
<%= textarea(f, :domain,
id: "snippet_code",
class:
"transition overflow-hidden bg-gray-100 dark:bg-gray-900 appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-400 dark:focus:border-gray-500 font-mono mt-4 resize-none text-xs",
value: render_snippet(@site),
rows: 3,
readonly: "readonly"
) %>
<a
onclick="var textarea = document.getElementById('snippet_code'); textarea.focus(); textarea.select(); document.execCommand('copy');"
href="javascript:void(0)"
class="no-underline text-indigo-500 hover:underline"
>
<svg
class="absolute text-indigo-500"
style="top: 24px; right: 12px;"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</a>
</div>
</div>
<% button_label =
if Plausible.Verification.enabled?() do
"Verify your integration to start collecting data"
else
"Start collecting data"
end %>
<%= link(button_label,
class: "button mt-4 w-full",
to: @form_submit_url
) %>
<% end %>
</PlausibleWeb.Components.Generic.focus_box>
<div class="w-full max-w-3xl mt-4 mx-auto flex"></div>

View File

@ -1,23 +0,0 @@
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= form_for @conn, "/", [class: "max-w-lg w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-bold dark:text-gray-100">Change JavaScript snippet</h2>
<div class="my-4">
<p class="dark:text-gray-100">Replace your snippet in the <code>&lt;head&gt;</code> of your website.</p>
<div class="relative">
<%= textarea f, :domain, id: "snippet_code", class: "transition overflow-hidden bg-gray-100 dark:bg-gray-900 appearance-none border border-transparent rounded w-full p-2 pr-6 text-gray-700 dark:text-gray-300 leading-normal appearance-none focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-400 dark:focus:border-gray-500 text-xs mt-4 resize-none", value: render_snippet(@site), rows: 3, readonly: "readonly" %>
<a onclick="var textarea = document.getElementById('snippet_code'); textarea.focus(); textarea.select(); document.execCommand('copy');" href="javascript:void(0)" class="no-underline text-indigo-500 text-sm hover:underline">
<svg class="absolute text-indigo-500" style="top: 24px; right: 12px;" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</a>
</div>
</div>
<p class="text-sm sm:text-sm text-gray-700 dark:text-gray-300">
<span class="font-bold dark:text-gray-100">Your domain has been changed. You must update the JavaScript snippet on your site within 72 hours to guarantee continuous tracking</span>. If you're using the API, please also make sure to update your API credentials.</p>
<p class="text-sm sm:text-sm text-gray-700 dark:text-gray-300 mt-4">
Visit our <a target="_blank" href="https://plausible.io/docs/change-domain-name/" class="text-indigo-500">documentation</a> for details.
</p>
<%= link("I understand, I'll change my snippet →", class: "button mt-4 w-full", to: "/#{URI.encode_www_form(@site.domain)}") %>
<% end %>
</div>

View File

@ -1,17 +0,0 @@
<%= form_for @conn, "/share/#{@link.slug}/authenticate", [class: "max-w-md w-full mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
<h2 class="text-xl font-black dark:text-gray-100">Enter password</h2>
<div class="my-4 dark:text-gray-100">
This link is password-protected. Please enter the password to continue to the dashboard.
</div>
<div class="my-6">
<%= label f, :password, "Password", class: "block text-sm font-bold dark:text-gray-100" %>
<%= password_input f, :password, class: "transition mt-3 bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500" %>
<%= if @conn.assigns[:error] do %>
<div class="text-red-500 text-xs italic mt-4"><%= @conn.assigns[:error] %></div>
<% end %>
</div>
<%= submit "Continue", class: "button mt-4 w-full" %>
<% end %>

View File

@ -0,0 +1,21 @@
<PlausibleWeb.Components.Generic.focus_box>
<:title>Enter password</:title>
<:subtitle>
This link is password-protected. Please enter the password to continue to the dashboard.
</:subtitle>
<%= form_for @conn, "/share/#{@link.slug}/authenticate", fn f -> %>
<div class="my-6">
<%= label(f, :password, "Password", class: "block dark:text-gray-100") %>
<%= password_input(f, :password,
class:
"transition mt-3 bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500"
) %>
<%= if @conn.assigns[:error] do %>
<div class="text-red-500 text-xs italic mt-4"><%= @conn.assigns[:error] %></div>
<% end %>
</div>
<%= submit("Continue", class: "button mt-4 w-full") %>
<% end %>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -1,78 +0,0 @@
<script>
function updateStatus() {
fetch("/api/<%= URI.encode_www_form(@site.domain) %>/status")
.then(function(res) { return res.json() })
.then(function(status) {
if (status === "READY") {
window.location.reload()
}
})
}
setInterval(updateStatus, 5000)
</script>
<PlausibleWeb.Components.FlowProgress.render
flow={@conn.params["flow"]}
current_step="Verify snippet"
/>
<%= if @site.locked do %>
<div
class="w-full px-4 py-4 text-sm font-bold text-center text-yellow-800 bg-yellow-100 rounded transition"
style="top: 91px"
role="alert"
>
<p>This dashboard is actually locked. You are viewing it with super-admin access</p>
</div>
<% end %>
<div
:if={not Plausible.Verification.enabled?()}
class="bg-white dark:bg-gray-800 shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-16 relative text-center"
>
<h2 class="text-xl font-bold dark:text-gray-100">Waiting for first pageview</h2>
<h2 class="text-xl font-bold dark:text-gray-100">on <%= @site.domain %></h2>
<div class="my-44">
<div class="block pulsating-circle top-1/2 left-1/2"></div>
<p class="text-gray-600 dark:text-gray-400 text-xs absolute left-0 bottom-0 mb-6 w-full text-center leading-normal">
Need to see the snippet again?
<.styled_link href={"/#{URI.encode_www_form(@site.domain)}/snippet?flow=#{@conn.params[~s|flow|]}"}>
Click here
</.styled_link>
<br /> Not working?
<.styled_link
new_tab
href="https://plausible.io/docs/troubleshoot-integration#check-for-the-plausible-snippet-in-your-source-code"
>
Troubleshoot the integration
</.styled_link>
<br />
<span :if={ee?()}>
Check the
<.styled_link href={Routes.site_path(@conn, :settings_general, @site.domain)}>
site settings
</.styled_link>
to invite team members, <br /> import historical stats and more.
</span>
<span :if={ce?()}>
Still not working? Ask on our
<.styled_link new_tab href="https://github.com/plausible/analytics/discussions">
community-supported forum
</.styled_link>
</span>
</p>
</div>
</div>
<%= if Plausible.Verification.enabled?(),
do:
live_render(@conn, PlausibleWeb.Live.Verification,
session: %{
"site_id" => @site.id,
"domain" => @site.domain,
"slowdown" => @conn.private[:verification_slowdown],
"flow" => @conn.params["flow"]
}
) %>

View File

@ -1,4 +0,0 @@
<div class="w-full max-w-md mx-auto bg-white dark:bg-gray-800 shadow-md rounded px-8 py-6 mt-8"]>
<h2 class="dark:text-gray-100">Unsubscribe successful</h2>
<p class="mt-4 dark:text-gray-100">You will no longer receive a <%= @interval %> analytics report for <%= @site %></p>
</div>

View File

@ -0,0 +1,9 @@
<PlausibleWeb.Components.Generic.focus_box>
<:title>
Unsubscribe successful
</:title>
<:subtitle>
You will no longer receive a <%= @interval %> analytics report for <%= @site.domain %>
</:subtitle>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -99,13 +99,13 @@ defmodule Plausible.Workers.TrafficChangeNotifier do
defp send_drop_notification(recipient, site, current_visitors) do defp send_drop_notification(recipient, site, current_visitors) do
site = Repo.preload(site, :members) site = Repo.preload(site, :members)
{dashboard_link, verification_link} = {dashboard_link, installation_link} =
if Enum.any?(site.members, &(&1.email == recipient)) do if Enum.any?(site.members, &(&1.email == recipient)) do
{ {
Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, []), Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, []),
Routes.site_url(PlausibleWeb.Endpoint, :settings_general, site.domain, Routes.site_url(PlausibleWeb.Endpoint, :installation, site.domain,
launch_verification: true flow: PlausibleWeb.Flows.review()
) <> "#snippet" )
} }
else else
{nil, nil} {nil, nil}
@ -117,7 +117,7 @@ defmodule Plausible.Workers.TrafficChangeNotifier do
site, site,
current_visitors, current_visitors,
dashboard_link, dashboard_link,
verification_link installation_link
) )
Plausible.Mailer.send(template) Plausible.Mailer.send(template)

View File

@ -544,7 +544,7 @@ defmodule Plausible.Verification.ChecksTest do
</html> </html>
""" """
test "disallowd via content-security-policy and GTM should make CSP a priority" do test "disallowed via content-security-policy and GTM should make CSP a priority" do
stub_fetch_body(fn conn -> stub_fetch_body(fn conn ->
conn conn
|> put_resp_header("content-security-policy", "default-src 'self' foo.local") |> put_resp_header("content-security-policy", "default-src 'self' foo.local")

View File

@ -27,18 +27,18 @@ defmodule PlausibleWeb.Components.FlowProgressTest do
test "register" do test "register" do
rendered = rendered =
render_component(&FlowProgress.render/1, render_component(&FlowProgress.render/1,
flow: "register", flow: PlausibleWeb.Flows.register(),
current_step: "Register" current_step: "Register"
) )
assert text_of_element(rendered, "#flow-progress") == assert text_of_element(rendered, "#flow-progress") ==
"1 Register 2 Activate account 3 Add site info 4 Install snippet 5 Verify snippet" "1 Register 2 Activate account 3 Add site info 4 Install Plausible 5 Verify installation"
end end
test "invitation" do test "invitation" do
rendered = rendered =
render_component(&FlowProgress.render/1, render_component(&FlowProgress.render/1,
flow: "invitation", flow: PlausibleWeb.Flows.invitation(),
current_step: "Register" current_step: "Register"
) )
@ -49,11 +49,33 @@ defmodule PlausibleWeb.Components.FlowProgressTest do
test "provisioning" do test "provisioning" do
rendered = rendered =
render_component(&FlowProgress.render/1, render_component(&FlowProgress.render/1,
flow: "provisioning", flow: PlausibleWeb.Flows.provisioning(),
current_step: "Add site info" current_step: "Add site info"
) )
assert text_of_element(rendered, "#flow-progress") == assert text_of_element(rendered, "#flow-progress") ==
"1 Add site info 2 Install snippet 3 Verify snippet" "1 Add site info 2 Install Plausible 3 Verify installation"
end
test "review" do
rendered =
render_component(&FlowProgress.render/1,
flow: PlausibleWeb.Flows.review(),
current_step: "Install Plausible"
)
assert text_of_element(rendered, "#flow-progress") ==
"1 Install Plausible 2 Verify installation"
end
test "domain_change" do
rendered =
render_component(&FlowProgress.render/1,
flow: PlausibleWeb.Flows.domain_change(),
current_step: "Set up new domain"
)
assert text_of_element(rendered, "#flow-progress") ==
"1 Set up new domain 2 Install Plausible 3 Verify installation"
end end
end end

View File

@ -2,41 +2,6 @@ defmodule PlausibleWeb.Api.InternalControllerTest do
use PlausibleWeb.ConnCase, async: true use PlausibleWeb.ConnCase, async: true
use Plausible.Repo use Plausible.Repo
describe "GET /api/:domain/status" do
setup [:create_user, :log_in]
test "is WAITING when site has no pageviews", %{conn: conn, user: user} do
site = insert(:site, members: [user])
conn = get(conn, "/api/#{site.domain}/status")
assert json_response(conn, 200) == "WAITING"
end
test "is READY when site has at least 1 pageview", %{conn: conn, user: user} do
site = insert(:site, members: [user])
Plausible.TestUtils.create_pageviews([%{site: site}])
conn = get(conn, "/api/#{site.domain}/status")
assert json_response(conn, 200) == "READY"
end
test "is WAITING when unauthenticated", %{user: user} do
site = insert(:site, members: [user])
Plausible.TestUtils.create_pageviews([%{site: site}])
conn = get(build_conn(), "/api/#{site.domain}/status")
assert json_response(conn, 200) == "WAITING"
end
test "is WAITING when non-existing site", %{conn: conn} do
conn = get(conn, "/api/example.com/status")
assert json_response(conn, 200) == "WAITING"
end
end
describe "GET /api/sites" do describe "GET /api/sites" do
setup [:create_user, :log_in] setup [:create_user, :log_in]

View File

@ -1504,8 +1504,10 @@ defmodule PlausibleWeb.AuthControllerTest do
assert element_exists?(html, "input[name=code]") assert element_exists?(html, "input[name=code]")
assert text_of_attr(html, "form#start-over-form", "action") == assert element_exists?(
Routes.auth_path(conn, :initiate_2fa_setup) html,
~s|a[data-method="post"][data-to="#{Routes.auth_path(conn, :initiate_2fa_setup)}"|
)
end end
test "redirects back to settings if 2FA not initiated", %{conn: conn} do test "redirects back to settings if 2FA not initiated", %{conn: conn} do

View File

@ -243,7 +243,7 @@ defmodule PlausibleWeb.SiteControllerTest do
}) })
assert redirected_to(conn) == assert redirected_to(conn) ==
"/#{URI.encode_www_form("éxample.com")}/snippet?site_created=true&flow=" "/#{URI.encode_www_form("éxample.com")}/installation?site_created=true&flow="
assert site = Repo.get_by(Plausible.Site, domain: "éxample.com") assert site = Repo.get_by(Plausible.Site, domain: "éxample.com")
assert site.timezone == "Europe/London" assert site.timezone == "Europe/London"
@ -341,7 +341,7 @@ defmodule PlausibleWeb.SiteControllerTest do
} }
}) })
assert redirected_to(conn) == "/example.com/snippet?site_created=true&flow=" assert redirected_to(conn) == "/example.com/installation?site_created=true&flow="
assert Repo.get_by(Plausible.Site, domain: "example.com") assert Repo.get_by(Plausible.Site, domain: "example.com")
end end
@ -361,7 +361,7 @@ defmodule PlausibleWeb.SiteControllerTest do
} }
}) })
assert redirected_to(conn) == "/example.com/snippet?site_created=true&flow=" assert redirected_to(conn) == "/example.com/installation?site_created=true&flow="
assert Plausible.Billing.Quota.Usage.site_usage(user) == 3 assert Plausible.Billing.Quota.Usage.site_usage(user) == 3
end end
@ -375,7 +375,7 @@ defmodule PlausibleWeb.SiteControllerTest do
} }
}) })
assert redirected_to(conn) == "/example.com/snippet?site_created=true&flow=" assert redirected_to(conn) == "/example.com/installation?site_created=true&flow="
assert Repo.get_by(Plausible.Site, domain: "example.com") assert Repo.get_by(Plausible.Site, domain: "example.com")
end end
end end
@ -457,17 +457,20 @@ defmodule PlausibleWeb.SiteControllerTest do
}) })
assert redirected_to(conn) == assert redirected_to(conn) ==
"/example.com/snippet?site_created=true&flow=" "/example.com/installation?site_created=true&flow="
end end
end end
describe "GET /:website/snippet" do describe "GET /:website/installation" do
setup [:create_user, :log_in, :create_site] setup [:create_user, :log_in, :create_site]
test "shows snippet", %{conn: conn, site: site} do test "static render - spinner determining installation type", %{
conn = get(conn, "/#{site.domain}/snippet") conn: conn,
site: site
} do
conn = get(conn, "/#{site.domain}/installation")
assert html_response(conn, 200) =~ "Add JavaScript snippet" assert html_response(conn, 200) =~ "Determining installation type"
end end
end end
@ -482,7 +485,7 @@ defmodule PlausibleWeb.SiteControllerTest do
assert resp =~ "Site Timezone" assert resp =~ "Site Timezone"
assert resp =~ "Site Domain" assert resp =~ "Site Domain"
assert resp =~ "JavaScript Snippet" assert resp =~ "Site Installation"
end end
end end
@ -1596,8 +1599,8 @@ defmodule PlausibleWeb.SiteControllerTest do
resp = html_response(conn, 200) resp = html_response(conn, 200)
assert resp =~ Routes.site_path(conn, :change_domain_submit, site.domain) assert resp =~ Routes.site_path(conn, :change_domain_submit, site.domain)
assert resp =~ assert text(resp) =~
"Once you change your domain, you must update the JavaScript snippet on your site within 72 hours" "Once you change your domain, you must update Plausible Installation on your site within 72 hours"
end end
test "domain change form submission when no change is made", %{conn: conn, site: site} do test "domain change form submission when no change is made", %{conn: conn, site: site} do
@ -1645,7 +1648,7 @@ defmodule PlausibleWeb.SiteControllerTest do
assert is_nil(site.domain_changed_from) assert is_nil(site.domain_changed_from)
end end
test "domain change successful form submission redirects to snippet change info", %{ test "domain change successful form submission redirects to installation", %{
conn: conn, conn: conn,
site: site site: site
} do } do
@ -1658,31 +1661,14 @@ defmodule PlausibleWeb.SiteControllerTest do
}) })
assert redirected_to(conn) == assert redirected_to(conn) ==
Routes.site_path(conn, :add_snippet_after_domain_change, new_domain) Routes.site_path(conn, :installation, new_domain,
flow: PlausibleWeb.Flows.domain_change()
)
site = Repo.reload!(site) site = Repo.reload!(site)
assert site.domain == new_domain assert site.domain == new_domain
assert site.domain_changed_from == original_domain assert site.domain_changed_from == original_domain
end end
test "snippet info after domain change", %{
conn: conn,
site: site
} do
put(conn, Routes.site_path(conn, :change_domain_submit, site.domain), %{
"site" => %{"domain" => "foo.example.com"}
})
resp =
conn
|> get(Routes.site_path(conn, :add_snippet_after_domain_change, "foo.example.com"))
|> html_response(200)
|> Floki.parse_document!()
|> Floki.text()
assert resp =~
"Your domain has been changed. You must update the JavaScript snippet on your site within 72 hours"
end
end end
describe "reset stats" do describe "reset stats" do

View File

@ -53,11 +53,24 @@ defmodule PlausibleWeb.StatsControllerTest do
assert text_of_element(resp, "title") == "Plausible Analytics: Live Demo" assert text_of_element(resp, "title") == "Plausible Analytics: Live Demo"
end end
test "public site - shows waiting for first pageview", %{conn: conn} do test "public site - redirect to /login when no stats because verification requires it", %{
conn: conn
} do
insert(:site, domain: "some-other-public-site.io", public: true) insert(:site, domain: "some-other-public-site.io", public: true)
conn = get(conn, "/some-other-public-site.io") conn = get(conn, conn |> get("/some-other-public-site.io") |> redirected_to())
assert html_response(conn, 200) =~ "Need to see the snippet again?" assert redirected_to(conn) == "/login"
end
test "public site - no stats with skip_to_dashboard", %{
conn: conn
} do
insert(:site, domain: "some-other-public-site.io", public: true)
conn = get(conn, "/some-other-public-site.io?skip_to_dashboard=true")
resp = html_response(conn, 200)
assert text_of_attr(resp, @react_container, "data-logged-in") == "false"
end end
test "can not view stats of a private website", %{conn: conn} do test "can not view stats of a private website", %{conn: conn} do
@ -81,7 +94,7 @@ defmodule PlausibleWeb.StatsControllerTest do
conn: conn, conn: conn,
site: site site: site
} do } do
resp = conn |> get("/" <> site.domain) |> html_response(200) resp = conn |> get(conn |> get("/" <> site.domain) |> redirected_to()) |> html_response(200)
refute text_of_attr(resp, @react_container, "data-logged-in") == "true" refute text_of_attr(resp, @react_container, "data-logged-in") == "true"
resp = conn |> get("/" <> site.domain <> "?skip_to_dashboard=true") |> html_response(200) resp = conn |> get("/" <> site.domain <> "?skip_to_dashboard=true") |> html_response(200)
@ -101,7 +114,7 @@ defmodule PlausibleWeb.StatsControllerTest do
end end
test "does not show CRM link to the site", %{conn: conn, site: site} do test "does not show CRM link to the site", %{conn: conn, site: site} do
conn = get(conn, "/" <> site.domain) conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to())
refute html_response(conn, 200) =~ "/crm/sites/site/#{site.id}" refute html_response(conn, 200) =~ "/crm/sites/site/#{site.id}"
end end
end end
@ -118,11 +131,11 @@ defmodule PlausibleWeb.StatsControllerTest do
assert html_response(conn, 200) =~ "stats-react-container" assert html_response(conn, 200) =~ "stats-react-container"
end end
test "can view a private dashboard without stats", %{conn: conn} do test "can enter verification when site is without stats", %{conn: conn} do
site = insert(:site) site = insert(:site)
conn = get(conn, "/" <> site.domain) conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to())
assert html_response(conn, 200) =~ "Need to see the snippet again?" assert html_response(conn, 200) =~ "Verifying your installation"
end end
test "can view a private locked dashboard with stats", %{conn: conn} do test "can view a private locked dashboard with stats", %{conn: conn} do
@ -138,13 +151,12 @@ defmodule PlausibleWeb.StatsControllerTest do
assert Enum.all?(attrs, fn {k, v} -> is_binary(k) and is_binary(v) end) assert Enum.all?(attrs, fn {k, v} -> is_binary(k) and is_binary(v) end)
end end
test "can view a private locked dashboard without stats", %{conn: conn} do test "can view private locked verification without stats", %{conn: conn} do
user = insert(:user) user = insert(:user)
site = insert(:site, locked: true, members: [user]) site = insert(:site, locked: true, members: [user])
conn = get(conn, "/" <> site.domain) conn = get(conn, conn |> get("/#{site.domain}") |> redirected_to())
assert html_response(conn, 200) =~ "Need to see the snippet again?" assert html_response(conn, 200) =~ "Verifying your installation"
assert html_response(conn, 200) =~ "This dashboard is actually locked"
end end
test "can view a locked public dashboard", %{conn: conn} do test "can view a locked public dashboard", %{conn: conn} do
@ -160,7 +172,7 @@ defmodule PlausibleWeb.StatsControllerTest do
test "shows CRM link to the site", %{conn: conn} do test "shows CRM link to the site", %{conn: conn} do
site = insert(:site) site = insert(:site)
conn = get(conn, "/" <> site.domain) conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to())
assert html_response(conn, 200) =~ "/crm/sites/site/#{site.id}" assert html_response(conn, 200) =~ "/crm/sites/site/#{site.id}"
end end
end end

View File

@ -18,8 +18,6 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
assert text_of_element(html, @progress) == assert text_of_element(html, @progress) ==
"We're visiting your site to ensure that everything is working" "We're visiting your site to ensure that everything is working"
assert element_exists?(html, ~s|a[href="/example.com/snippet?flow="]|)
assert element_exists?(html, ~s|a[href="/example.com/settings/general"]|)
assert element_exists?(html, @pulsating_circle) assert element_exists?(html, @pulsating_circle)
refute class_of_element(html, @pulsating_circle) =~ "hidden" refute class_of_element(html, @pulsating_circle) =~ "hidden"
refute element_exists?(html, @recommendations) refute element_exists?(html, @recommendations)
@ -55,11 +53,10 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
] ]
end end
test "hides pulsating circle when finished in a modal, shows check circle" do test "hides pulsating circle when finished, shows check circle" do
html = html =
render_component(@component, render_component(@component,
domain: "example.com", domain: "example.com",
modal?: true,
success?: true, success?: true,
finished?: true finished?: true
) )
@ -77,11 +74,29 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
@tag :ee_only @tag :ee_only
test "renders contact link on >3 attempts" do test "renders contact link on >3 attempts" do
html = render_component(@component, domain: "example.com", attempts: 2, finished?: true) html = render_component(@component, domain: "example.com", attempts: 2, finished?: true)
refute html =~ "Need further help with your integration?" refute html =~ "Need further help with your installation?"
refute element_exists?(html, ~s|a[href="https://plausible.io/contact"]|) refute element_exists?(html, ~s|a[href="https://plausible.io/contact"]|)
html = render_component(@component, domain: "example.com", attempts: 3, finished?: true) html = render_component(@component, domain: "example.com", attempts: 3, finished?: true)
assert html =~ "Need further help with your integration?" assert html =~ "Need further help with your installation?"
assert element_exists?(html, ~s|a[href="https://plausible.io/contact"]|) assert element_exists?(html, ~s|a[href="https://plausible.io/contact"]|)
end end
test "offers escape paths: settings and installation instructions on failure" do
html =
render_component(@component,
domain: "example.com",
success?: false,
finished?: true,
installation_type: "WordPress",
flow: PlausibleWeb.Flows.review()
)
assert element_exists?(html, ~s|a[href="/example.com/settings/general"]|)
assert element_exists?(
html,
~s|a[href="/example.com/installation?flow=review&installation_type=WordPress"]|
)
end
end end

View File

@ -0,0 +1,348 @@
defmodule PlausibleWeb.Live.InstallationTest do
use PlausibleWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
setup [:create_user, :log_in, :create_site]
describe "GET /:domain/installation" do
test "static verification screen renders", %{conn: conn, site: site} do
resp = get(conn, "/#{site.domain}/installation") |> html_response(200)
assert resp =~ "Determining installation type"
refute resp =~ "Review your existing installation."
end
test "static verification screen renders for flow=review", %{conn: conn, site: site} do
resp =
conn
|> get("/#{site.domain}/installation?flow=review&installation_type=manual")
|> html_response(200)
assert resp =~ "Review your existing installation."
assert resp =~ "Verify your installation"
assert resp =~
Routes.site_path(PlausibleWeb.Endpoint, :verification, site.domain,
flow: PlausibleWeb.Flows.review()
)
end
test "static verification screen renders for flow=domain_change", %{conn: conn, site: site} do
resp =
conn
|> get("/#{site.domain}/installation?flow=#{PlausibleWeb.Flows.domain_change()}")
|> html_response(200)
assert resp =~ "Your domain has been changed"
assert resp =~ "I understand, I'll update my website"
assert resp =~ "Manual installation"
refute resp =~ "Review your existing installation."
assert resp =~
Routes.site_path(PlausibleWeb.Endpoint, :verification, site.domain,
flow: PlausibleWeb.Flows.domain_change()
)
end
test "static verification screen renders for flow=domain_change using original installation type",
%{conn: conn, site: site} do
site = Plausible.Sites.update_installation_meta!(site, %{installation_type: "WordPress"})
resp =
conn
|> get("/#{site.domain}/installation?flow=#{PlausibleWeb.Flows.domain_change()}")
|> html_response(200)
assert resp =~ "Your domain has been changed"
assert resp =~ "I understand, I'll update my website"
assert resp =~ "WordPress plugin"
refute resp =~ "Manuial installation"
refute resp =~ "Review your existing installation."
assert resp =~
Routes.site_path(PlausibleWeb.Endpoint, :verification, site.domain,
flow: PlausibleWeb.Flows.domain_change()
)
end
test "renders pre-determined installation type: WordPress", %{conn: conn, site: site} do
resp =
conn
|> get("/#{site.domain}/installation?installation_type=WordPress")
|> html_response(200)
assert resp =~ "Install WordPress plugin"
assert resp =~ "Start collecting data"
refute resp =~ "Review your existing installation."
assert resp =~
Routes.site_path(PlausibleWeb.Endpoint, :verification, site.domain,
installation_type: "WordPress"
)
end
test "renders pre-determined installation type: GTM", %{conn: conn, site: site} do
resp =
conn |> get("/#{site.domain}/installation?installation_type=GTM") |> html_response(200)
assert resp =~ "Install Google Tag Manager"
assert resp =~ "Start collecting data"
refute resp =~ "Review your existing installation."
assert resp =~
Routes.site_path(PlausibleWeb.Endpoint, :verification, site.domain,
installation_type: "GTM"
)
end
test "renders pre-determined installation type: manual", %{conn: conn, site: site} do
resp =
conn |> get("/#{site.domain}/installation?installation_type=manual") |> html_response(200)
assert resp =~ "Manual installation"
assert resp =~ "Start collecting data"
refute resp =~ "Review your existing installation."
assert resp =~
Routes.site_path(PlausibleWeb.Endpoint, :verification, site.domain,
installation_type: "manual"
)
end
test "ignores unknown installation types", %{conn: conn, site: site} do
resp =
conn |> get("/#{site.domain}/installation?installation_type=UM_NO") |> html_response(200)
assert resp =~ "Determining installation type"
end
end
describe "LiveView" do
test "mounts and detects installation type", %{conn: conn, site: site} do
stub_fetch_body(200, "wp-content")
{lv, _} = get_lv(conn, site)
assert eventually(fn ->
html = render(lv)
{
text(html) =~ "Install WordPress",
html
}
end)
_ = render(lv)
end
@tag :slow
test "mounts and does not detect installation type, if it's provided", %{
conn: conn,
site: site
} do
stub_fetch_body(200, "wp-content")
{lv, _} = get_lv(conn, site, "?installation_type=GTM")
refute eventually(fn ->
html = render(lv)
{
text(html) =~ "Install WordPress",
html
}
end)
_ = render(lv)
end
test "allows manual snippet customization", %{conn: conn, site: site} do
{lv, html} = get_lv(conn, site, "?installation_type=manual")
assert text_of_element(html, "textarea#snippet") ==
"&amp;lt;script defer data-domain=&amp;quot;#{site.domain}&amp;quot; src=&amp;quot;http://localhost:8000/js/script.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;"
for param <- PlausibleWeb.Live.Installation.script_extension_params() do
lv
|> element(~s|form#snippet-form|)
|> render_change(%{
param => "on"
})
html = lv |> render()
assert text_of_element(html, "textarea#snippet") =~ "/js/script.#{param}.js"
lv
|> element(~s|form#snippet-form|)
|> render_change(%{})
html = lv |> render()
assert text_of_element(html, "textarea#snippet") =~ "/js/script.js"
assert html =~ "Snippet updated"
end
end
test "allows GTM snippet customization", %{conn: conn, site: site} do
{lv, html} = get_lv(conn, site, "?installation_type=GTM")
assert text_of_element(html, "textarea#snippet") =~ "script.defer = true"
for param <- PlausibleWeb.Live.Installation.script_extension_params() do
lv
|> element(~s|form#snippet-form|)
|> render_change(%{
param => "on"
})
html = lv |> render()
assert text_of_element(html, "textarea#snippet") =~ "/js/script.#{param}.js"
lv
|> element(~s|form#snippet-form|)
|> render_change(%{})
html = lv |> render()
assert text_of_element(html, "textarea#snippet") =~ "/js/script.js"
assert html =~ "Snippet updated"
end
end
test "allows manual snippet customization with 404 links", %{conn: conn, site: site} do
{lv, _html} = get_lv(conn, site, "?installation_type=manual")
lv
|> element(~s|form#snippet-form|)
|> render_change(%{
"404" => "on"
})
html = lv |> render()
assert text_of_element(html, "textarea#snippet") =~
"function() { (window.plausible.q = window.plausible.q || []).push(arguments) }&amp;lt;/script&amp;gt;"
lv
|> element(~s|form#snippet-form|)
|> render_change(%{})
html = lv |> render()
refute text_of_element(html, "textarea#snippet") =~
"function() { (window.plausible.q = window.plausible.q || []).push(arguments) }&amp;lt;/script&amp;gt;"
end
test "turning on file-downloads, outbound-links and 404 creates special goals", %{
conn: conn,
site: site
} do
{lv, _html} = get_lv(conn, site, "?installation_type=manual")
assert Plausible.Goals.for_site(site) == []
lv
|> element(~s|form#snippet-form|)
|> render_change(%{
"file-downloads" => "on",
"outbound-links" => "on",
"404" => "on"
})
lv |> render()
assert [clicks, downloads, error_404] = Plausible.Goals.for_site(site)
assert clicks.event_name == "Outbound Link: Click"
assert downloads.event_name == "File Download"
assert error_404.event_name == "404"
end
test "turning off file-downloads, outbound-links and 404 deletes special goals", %{
conn: conn,
site: site
} do
{lv, _html} = get_lv(conn, site, "?installation_type=manual")
assert Plausible.Goals.for_site(site) == []
lv
|> element(~s|form#snippet-form|)
|> render_change(%{
"file-downloads" => "on",
"outbound-links" => "on",
"404" => "on"
})
assert [_, _, _] = Plausible.Goals.for_site(site)
lv
|> element(~s|form#snippet-form|)
|> render_change(%{
"file-downloads" => "on",
"outbound-links" => "on"
})
assert render(lv) =~ "Snippet updated and goal deleted"
lv
|> element(~s|form#snippet-form|)
|> render_change(%{
"file-downloads" => "on"
})
assert render(lv) =~ "Snippet updated and goal deleted"
lv
|> element(~s|form#snippet-form|)
|> render_change(%{})
assert render(lv) =~ "Snippet updated and goal deleted"
assert [] = Plausible.Goals.for_site(site)
end
test "turning off remaining checkboxes doesn't render goal deleted flash", %{
conn: conn,
site: site
} do
{lv, _html} = get_lv(conn, site, "?installation_type=manual")
lv
|> element(~s|form#snippet-form|)
|> render_change(%{
"tagged-events" => "on",
"hash" => "on",
"pageview-props" => "on",
"revenue" => "on"
})
assert render(lv) =~ "Snippet updated. Please insert the newest snippet into your site"
lv
|> element(~s|form#snippet-form|)
|> render_change(%{})
assert render(lv) =~ "Snippet updated. Please insert the newest snippet into your site"
end
end
defp stub_fetch_body(f) when is_function(f, 1) do
Req.Test.stub(Plausible.Verification.Checks.FetchBody, f)
end
defp stub_fetch_body(status, body) do
stub_fetch_body(fn conn ->
conn
|> put_resp_content_type("text/html")
|> send_resp(status, body)
end)
end
defp get_lv(conn, site, qs \\ nil) do
{:ok, lv, html} = live(conn, "/#{site.domain}/installation#{qs}")
{lv, html}
end
end

View File

@ -9,7 +9,7 @@ defmodule PlausibleWeb.Live.PluginsAPISettingsTest do
setup [:create_user, :log_in, :create_site] setup [:create_user, :log_in, :create_site]
test "does not display the Plugins API section by default", %{conn: conn, site: site} do test "does not display the Plugins API section by default", %{conn: conn, site: site} do
conn = get(conn, "/#{site.domain}/integrations") conn = get(conn, "/#{site.domain}/settings/integrations")
resp = html_response(conn, 200) resp = html_response(conn, 200)
refute resp =~ "Plugin Tokens" refute resp =~ "Plugin Tokens"

View File

@ -6,46 +6,33 @@ defmodule PlausibleWeb.Live.VerificationTest do
setup [:create_user, :log_in, :create_site] setup [:create_user, :log_in, :create_site]
@verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]| # @verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]|
@verification_modal ~s|div#verification-modal|
@retry_button ~s|a[phx-click="retry"]| @retry_button ~s|a[phx-click="retry"]|
@go_to_dashboard_button ~s|a[href$="?skip_to_dashboard=true"]| # @go_to_dashboard_button ~s|a[href$="?skip_to_dashboard=true"]|
@progress ~s|#progress-indicator p#progress| @progress ~s|#progress-indicator p#progress|
@heading ~s|#progress-indicator h3| @heading ~s|#progress-indicator h3|
describe "GET /:domain" do describe "GET /:domain" do
test "static verification screen renders", %{conn: conn, site: site} do test "static verification screen renders", %{conn: conn, site: site} do
resp = conn |> no_slowdown() |> get("/#{site.domain}") |> html_response(200) resp =
get(conn, conn |> no_slowdown() |> get("/#{site.domain}") |> redirected_to)
|> html_response(200)
assert text_of_element(resp, @progress) =~ assert text_of_element(resp, @progress) =~
"We're visiting your site to ensure that everything is working" "We're visiting your site to ensure that everything is working"
assert resp =~ "Verifying your integration" assert resp =~ "Verifying your installation"
assert resp =~ "Need to see the snippet again?"
assert resp =~ "Run verification later and go to Site Settings?"
refute resp =~ "modal"
refute element_exists?(resp, @verification_modal)
end end
end end
describe "GET /settings/general" do describe "LiveView" do
test "verification elements render under the snippet", %{conn: conn, site: site} do
resp =
conn |> no_slowdown() |> get("/#{site.domain}/settings/general") |> html_response(200)
assert element_exists?(resp, @verify_button)
assert element_exists?(resp, @verification_modal)
end
end
describe "LiveView: standalone" do
test "LiveView mounts", %{conn: conn, site: site} do test "LiveView mounts", %{conn: conn, site: site} do
stub_fetch_body(200, "") stub_fetch_body(200, "")
stub_installation() stub_installation()
{_, html} = get_lv_standalone(conn, site) {_, html} = get_lv(conn, site)
assert html =~ "Verifying your integration" assert html =~ "Verifying your installation"
assert text_of_element(html, @progress) =~ assert text_of_element(html, @progress) =~
"We're visiting your site to ensure that everything is working" "We're visiting your site to ensure that everything is working"
@ -55,7 +42,7 @@ defmodule PlausibleWeb.Live.VerificationTest do
stub_fetch_body(200, source(site.domain)) stub_fetch_body(200, source(site.domain))
stub_installation() stub_installation()
{:ok, lv} = kick_off_live_verification_standalone(conn, site) {:ok, lv} = kick_off_live_verification(conn, site)
assert eventually(fn -> assert eventually(fn ->
html = render(lv) html = render(lv)
@ -69,14 +56,61 @@ defmodule PlausibleWeb.Live.VerificationTest do
html = render(lv) html = render(lv)
assert html =~ "Success!" assert html =~ "Success!"
assert html =~ "Your integration is working" assert html =~ "Awaiting your first pageview"
end
test "won't await first pageview if site has pageviews", %{conn: conn, site: site} do
populate_stats(site, [
build(:pageview)
])
stub_fetch_body(200, source(site.domain))
stub_installation()
{:ok, lv} = kick_off_live_verification(conn, site)
assert eventually(fn ->
html = render(lv)
{
text(html) =~ "Success",
html
}
end)
html = render(lv)
refute text_of_element(html, @progress) =~ "Awaiting your first pageview"
refute_redirected(lv, "http://localhost:8000/#{URI.encode_www_form(site.domain)}")
end
test "will redirect when first pageview arrives", %{conn: conn, site: site} do
stub_fetch_body(200, source(site.domain))
stub_installation()
{:ok, lv} = kick_off_live_verification(conn, site)
assert eventually(fn ->
html = render(lv)
{
text(html) =~ "Awaiting",
html
}
end)
populate_stats(site, [
build(:pageview)
])
assert_redirect(lv, "http://localhost:8000/#{URI.encode_www_form(site.domain)}/")
end end
test "eventually fails to verify installation", %{conn: conn, site: site} do test "eventually fails to verify installation", %{conn: conn, site: site} do
stub_fetch_body(200, "") stub_fetch_body(200, "")
stub_installation(200, plausible_installed(false)) stub_installation(200, plausible_installed(false))
{:ok, lv} = kick_off_live_verification_standalone(conn, site) {:ok, lv} = kick_off_live_verification(conn, site)
assert html = assert html =
eventually(fn -> eventually(fn ->
@ -90,133 +124,39 @@ defmodule PlausibleWeb.Live.VerificationTest do
} }
end) end)
refute element_exists?(html, @verification_modal)
assert element_exists?(html, @retry_button) assert element_exists?(html, @retry_button)
assert html =~ "Please insert the snippet into your site" assert html =~ "Please insert the snippet into your site"
end end
end end
describe "LiveView: modal" do defp get_lv(conn, site) do
test "LiveView mounts", %{conn: conn, site: site} do {:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification")
stub_fetch_body(200, "")
stub_installation()
{_, html} = get_lv_modal(conn, site)
text = text(html)
refute text =~ "Need to see the snippet again?"
refute text =~ "Run verification later and go to Site Settings?"
assert element_exists?(html, @verification_modal)
end
test "Clicking the Verify modal launches verification", %{conn: conn, site: site} do
stub_fetch_body(200, source(site.domain))
stub_installation()
{lv, html} = get_lv_modal(conn, site)
assert element_exists?(html, @verification_modal)
assert element_exists?(html, @verify_button)
assert text_of_attr(html, @verify_button, "x-on:click") =~ "open-modal"
assert text_of_element(html, @progress) =~
"We're visiting your site to ensure that everything is working"
lv |> element(@verify_button) |> render_click()
assert html =
eventually(fn ->
html = render(lv)
{
html =~ "Success!",
html
}
end)
refute html =~ "Awaiting your first pageview"
assert element_exists?(html, @go_to_dashboard_button)
end
test "query launch_verification=true launches the modal", %{conn: conn, site: site} do
stub_fetch_body(200, source(site.domain))
stub_installation()
{lv, _html} = get_lv_modal(conn, site, "?launch_verification=true")
assert html =
eventually(fn ->
html = render(lv)
{
html =~ "Success!",
html
}
end)
refute html =~ "Awaiting your first pageview"
assert element_exists?(html, @go_to_dashboard_button)
end
test "failed verification can be retried", %{conn: conn, site: site} do
stub_fetch_body(200, "")
stub_installation(200, plausible_installed(false))
{lv, _html} = get_lv_modal(conn, site)
lv |> element(@verify_button) |> render_click()
assert html =
eventually(fn ->
html = render(lv)
{text_of_element(html, @heading) =~
"We couldn't find the Plausible snippet", html}
end)
assert element_exists?(html, @retry_button)
stub_fetch_body(200, source(site.domain))
stub_installation()
lv |> element(@retry_button) |> render_click()
assert eventually(fn ->
html = render(lv)
{html =~ "Success!", html}
end)
end
end
defp get_lv_standalone(conn, site) do
conn = conn |> no_slowdown() |> assign(:live_module, PlausibleWeb.Live.Verification)
{:ok, lv, html} = live(conn, "/#{site.domain}")
{lv, html} {lv, html}
end end
defp get_lv_modal(conn, site, query_string \\ "") do defp kick_off_live_verification(conn, site) do
conn = conn |> no_slowdown() |> assign(:live_module, PlausibleWeb.Live.Verification) {:ok, lv, _html} = conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification")
{:ok, lv, html} = live(no_slowdown(conn), "/#{site.domain}/settings/general#{query_string}")
{lv, html}
end
defp kick_off_live_verification_standalone(conn, site) do
{:ok, lv, _} =
live_isolated(conn, PlausibleWeb.Live.Verification,
session: %{
"domain" => site.domain,
"delay" => 0,
"slowdown" => 0
}
)
# {:ok, lv, _} =
# live_isolated(conn, PlausibleWeb.Live.Verification,
# session: %{
# "domain" => site.domain,
# "delay" => 0,
# "slowdown" => 0
# }
# )
#
{:ok, lv} {:ok, lv}
end end
defp no_slowdown(conn) do defp no_slowdown(conn) do
Plug.Conn.put_private(conn, :verification_slowdown, 0) Plug.Conn.put_private(conn, :slowdown, 0)
end
defp no_delay(conn) do
Plug.Conn.put_private(conn, :delay, 0)
end end
defp stub_fetch_body(f) when is_function(f, 1) do defp stub_fetch_body(f) when is_function(f, 1) do

View File

@ -139,7 +139,9 @@ defmodule Plausible.Workers.TrafficChangeNotifierTest do
TrafficChangeNotifier.perform(nil, clickhouse_stub) TrafficChangeNotifier.perform(nil, clickhouse_stub)
assert_email_delivered_with(html_body: ~r|http://localhost:8000/example.com/settings|) assert_email_delivered_with(
html_body: ~r|http://localhost:8000/example.com/installation\?flow=review|
)
end end
end end