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,
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,
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")

View File

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

View File

@ -236,6 +236,57 @@ defmodule Plausible.Goals do
)
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
params = Map.delete(params, "site_id")

View File

@ -32,6 +32,10 @@ defmodule Plausible.Site do
# NOTE: needed by `SiteImports` data migration script
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
has_many :memberships, Plausible.Site.Membership
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
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
from(s in Site,
join: sm in Site.Membership,

View File

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

View File

@ -1,38 +1,16 @@
defmodule PlausibleWeb.Components.FlowProgress do
@moduledoc """
Component for provisioning/registration flows displaying
progress status.
progress status. See `PlausibleWeb.Flows` for the list of
flow definitions.
"""
use Phoenix.Component
@flows %{
"register" => [
"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
attr :flow, :string, required: true, values: PlausibleWeb.Flows.valid_keys()
attr :current_step, :string, required: true, values: PlausibleWeb.Flows.valid_values()
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))
assigns =

View File

@ -181,6 +181,7 @@ defmodule PlausibleWeb.Components.Generic do
attr :new_tab, :boolean, default: false
attr :class, :string, default: ""
attr :rest, :global
attr :method, :string, default: "get"
slot :inner_block
def styled_link(assigns) do
@ -188,6 +189,7 @@ defmodule PlausibleWeb.Components.Generic do
<.unstyled_link
new_tab={@new_tab}
href={@href}
method={@method}
class={"text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-600 " <> @class}
{@rest}
>
@ -263,9 +265,23 @@ defmodule PlausibleWeb.Components.Generic do
attr :class, :string, default: ""
attr :id, :any, default: nil
attr :rest, :global
attr :method, :string, default: "get"
slot :inner_block
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
assigns = assign(assigns, :icon_class, icon_class(assigns))
@ -279,6 +295,7 @@ defmodule PlausibleWeb.Components.Generic do
href={@href}
target="_blank"
rel="noopener noreferrer"
{@extra}
{@rest}
>
<%= render_slot(@inner_block) %>
@ -287,7 +304,7 @@ defmodule PlausibleWeb.Components.Generic do
"""
else
~H"""
<.link class={@class} href={@href} {@rest}>
<.link class={@class} href={@href} {@extra} {@rest}>
<%= render_slot(@inner_block) %>
</.link>
"""
@ -320,12 +337,22 @@ defmodule PlausibleWeb.Components.Generic do
attr :wrapper_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 :tooltip_content, required: true
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"""
<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
x-on:click="sticky = true; hovered = true"
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]}
>
<%= 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>
<span
x-show="hovered || sticky"
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"
x-show={@show_inner}
class={[
"bg-gray-900 pointer-events-none absolute transition-opacity p-4 rounded text-sm text-white",
@position
]}
>
<%= render_slot(List.first(@tooltip_content)) %>
</span>
@ -369,20 +399,27 @@ defmodule PlausibleWeb.Components.Generic do
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 :subtitle
slot :inner_block, required: true
slot :footer
attr :outer_markup, :boolean, default: true
def focus_box(assigns) do
~H"""
<div class={[
"bg-white w-full max-w-lg mx-auto dark:bg-gray-800 text-black dark:text-gray-100",
@outer_markup && "shadow-md rounded mb-4 mt-8"
]}>
<div class={[@outer_markup && "p-8"]}>
<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">
<div class="p-8">
<h2 :if={@title != []} class="text-xl font-black dark:text-gray-100">
<%= render_slot(@title) %>
</h2>
@ -403,7 +440,7 @@ defmodule PlausibleWeb.Components.Generic do
:if={@footer != []}
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) %>
</div>
</div>

View File

@ -1,22 +1,9 @@
defmodule PlausibleWeb.Api.InternalController do
use PlausibleWeb, :controller
use Plausible.Repo
alias Plausible.Stats.Clickhouse, as: Stats
alias Plausible.{Sites, Site, Auth}
alias Plausible.{Sites, Auth}
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
current_user = conn.assigns[:current_user]

View File

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

View File

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

View File

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

View File

@ -46,8 +46,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
expires_at: expires_at,
site: conn.assigns.site,
properties: properties,
selected_property_error: error,
layout: {PlausibleWeb.LayoutView, "focus.html"}
selected_property_error: error
)
{:error, :rate_limit_exceeded} ->
@ -182,8 +181,7 @@ defmodule PlausibleWeb.GoogleAnalyticsController do
selected_property: property,
selected_property_name: property_name,
start_date: start_date,
end_date: end_date,
layout: {PlausibleWeb.LayoutView, "focus.html"}
end_date: end_date
)
{: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.
"""
def index(conn, _params) do
render(conn, "index.html", layout: {PlausibleWeb.LayoutView, "focus.html"})
render(conn, "index.html")
end
end

View File

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

View File

@ -14,14 +14,13 @@ defmodule PlausibleWeb.SiteController do
)
def new(conn, params) do
flow = params["flow"] || "register"
flow = params["flow"] || PlausibleWeb.Flows.register()
current_user = conn.assigns[:current_user]
render(conn, "new.html",
changeset: Plausible.Site.changeset(%Plausible.Site{}),
site_limit: Quota.Limits.site_limit(current_user),
site_limit_exceeded?: Quota.ensure_can_add_new_site(current_user) != :ok,
layout: {PlausibleWeb.LayoutView, "focus.html"},
form_submit_url: "/sites?flow=#{flow}",
flow: flow
)
@ -41,7 +40,7 @@ defmodule PlausibleWeb.SiteController do
redirect(conn,
external:
Routes.site_path(conn, :add_snippet, site.domain,
Routes.site_path(conn, :installation, site.domain,
site_created: true,
flow: flow
)
@ -53,7 +52,6 @@ defmodule PlausibleWeb.SiteController do
first_site?: first_site?,
site_limit: limit,
site_limit_exceeded?: true,
layout: {PlausibleWeb.LayoutView, "focus.html"},
flow: flow,
form_submit_url: "/sites?flow=#{flow}"
)
@ -64,37 +62,12 @@ defmodule PlausibleWeb.SiteController do
first_site?: first_site?,
site_limit: Quota.Limits.site_limit(user),
site_limit_exceeded?: false,
layout: {PlausibleWeb.LayoutView, "focus.html"},
flow: flow,
form_submit_url: "/sites?flow=#{flow}"
)
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, %{
"setting" => setting,
"r" => "/" <> _ = redirect_path,
@ -585,8 +558,7 @@ defmodule PlausibleWeb.SiteController do
|> assign(:skip_plausible_tracking, true)
|> render("new_shared_link.html",
site: site,
changeset: changeset,
layout: {PlausibleWeb.LayoutView, "focus.html"}
changeset: changeset
)
end
@ -602,8 +574,7 @@ defmodule PlausibleWeb.SiteController do
|> assign(:skip_plausible_tracking, true)
|> render("new_shared_link.html",
site: site,
changeset: changeset,
layout: {PlausibleWeb.LayoutView, "focus.html"}
changeset: changeset
)
end
end
@ -617,8 +588,7 @@ defmodule PlausibleWeb.SiteController do
|> assign(:skip_plausible_tracking, true)
|> render("edit_shared_link.html",
site: site,
changeset: changeset,
layout: {PlausibleWeb.LayoutView, "focus.html"}
changeset: changeset
)
end
@ -636,8 +606,7 @@ defmodule PlausibleWeb.SiteController do
|> assign(:skip_plausible_tracking, true)
|> render("edit_shared_link.html",
site: site,
changeset: changeset,
layout: {PlausibleWeb.LayoutView, "focus.html"}
changeset: changeset
)
end
end
@ -750,7 +719,6 @@ defmodule PlausibleWeb.SiteController do
conn
|> assign(:skip_plausible_tracking, true)
|> render("csv_import.html",
layout: {PlausibleWeb.LayoutView, "focus.html"},
connect_live_socket: true
)
end
@ -760,8 +728,7 @@ defmodule PlausibleWeb.SiteController do
render(conn, "change_domain.html",
skip_plausible_tracking: true,
changeset: changeset,
layout: {PlausibleWeb.LayoutView, "focus.html"}
changeset: changeset
)
end
@ -771,29 +738,20 @@ defmodule PlausibleWeb.SiteController do
conn
|> put_flash(:success, "Website domain changed successfully")
|> 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} ->
render(conn, "change_domain.html",
skip_plausible_tracking: true,
changeset: changeset,
layout: {PlausibleWeb.LayoutView, "focus.html"}
changeset: changeset
)
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
case result do
{:ok, _} ->

View File

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

View File

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

View File

@ -141,7 +141,7 @@ defmodule PlausibleWeb.Email do
})
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()
|> to(email)
|> tag("drop-notification")
@ -150,7 +150,7 @@ defmodule PlausibleWeb.Email do
site: site,
current_visitors: current_visitors,
dashboard_link: dashboard_link,
settings_link: settings_link
installation_link: installation_link
})
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 Plausible
alias PlausibleWeb.Router.Helpers, as: Routes
import PlausibleWeb.Components.Generic
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"
@ -18,20 +19,22 @@ defmodule PlausibleWeb.Live.Components.Verification do
attr :interpretation, Plausible.Verification.Diagnostics.Result, default: nil
attr :attempts, :integer, default: 0
attr :flow, :string, default: ""
attr :installation_type, :string, default: nil
attr :awaiting_first_pageview?, :boolean, default: false
def render(assigns) do
~H"""
<div id="progress-indicator">
<PlausibleWeb.Components.Generic.focus_box outer_markup={not @modal?}>
<PlausibleWeb.Components.Generic.focus_box>
<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"
>
<div class="block pulsating-circle"></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"
id="check-circle"
>
@ -49,17 +52,21 @@ defmodule PlausibleWeb.Live.Components.Verification do
<div class="mt-6">
<h3 class="font-semibold leading-6 text-xl">
<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}>
<%= List.first(@interpretation.errors) %>
</span>
</h3>
<p :if={@finished? and @success? and @modal?} id="progress" class="mt-2">
Your integration is working and visitors are being counted accurately
<p :if={@finished? and @success?} id="progress" class="mt-2">
Your installation is working and visitors are being counted accurately
</p>
<p :if={@finished? and @success? and not @modal?} id="progress" class="mt-2 animate-pulse">
Your integration is working. Awaiting your first pageview.
<p
:if={@finished? and @success? and @awaiting_first_pageview?}
id="progress"
class="mt-2 animate-pulse"
>
Awaiting your first pageview
</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">
<.button_link :if={not @success?} href="#" phx-click="retry" class="w-full">
Verify integration again
Verify installation again
</.button_link>
<.button_link
:if={@success?}
@ -88,34 +95,32 @@ defmodule PlausibleWeb.Live.Components.Verification do
</.button_link>
</div>
<:footer :if={
(not @modal? and not @success?) or
(@finished? and not @success?)
}>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4">
<%= if ee?() and @finished? and not @success? and @attempts >= 3 do %>
<li>
<b>Need further help with your integration?</b>
<.styled_link href="https://plausible.io/contact">
Contact us
</.styled_link>
</li>
<% end %>
<%= if not @success? and not @modal? do %>
<li>
Need to see the snippet again?
<.styled_link href={"/#{URI.encode_www_form(@domain)}/snippet?flow=#{@flow}"}>
Click here
</.styled_link>
</li>
<li>
Run verification later and go to Site Settings?
<.styled_link href={"/#{URI.encode_www_form(@domain)}/settings/general"}>
Click here
</.styled_link>
</li>
<% end %>
</ol>
<:footer :if={@finished? and not @success?}>
<.focus_list>
<:item :if={ee?() and @attempts >= 3}>
<b>Need further help with your installation?</b>
<.styled_link href="https://plausible.io/contact">
Contact us
</.styled_link>
</:item>
<:item>
Need to see installation instructions again?
<.styled_link href={
Routes.site_path(PlausibleWeb.Endpoint, :installation, @domain,
flow: @flow,
installation_type: @installation_type
)
}>
Click here
</.styled_link>
</:item>
<:item>
Run verification later and go to site settings?
<.styled_link href={"/#{URI.encode_www_form(@domain)}/settings/general"}>
Click here
</.styled_link>
</:item>
</.focus_list>
</:footer>
</PlausibleWeb.Components.Generic.focus_box>
</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 Phoenix.HTML
import PlausibleWeb.Live.Components.Form
import PlausibleWeb.Components.Generic
alias Plausible.Auth
alias Plausible.Repo
@ -78,12 +79,12 @@ defmodule PlausibleWeb.Live.RegisterForm do
<PlausibleWeb.Components.FlowProgress.render
:if={@live_action == :register_form}
flow="register"
flow={PlausibleWeb.Flows.register()}
current_step="Register"
/>
<PlausibleWeb.Components.FlowProgress.render
:if={@live_action == :register_from_invitation_form}
flow="invitation"
flow={PlausibleWeb.Flows.invitation()}
current_step="Register"
/>
@ -188,10 +189,10 @@ defmodule PlausibleWeb.Live.RegisterForm do
</PlausibleWeb.Components.Generic.button>
<p class="text-center text-gray-600 dark:text-gray-500 mt-4">
Already have an account? <%= link("Log in",
to: "/login",
class: "underline text-gray-800 dark:text-gray-50"
) %>
Already have an account?
<.styled_link href="/login">
Log in
</.styled_link>
</p>
</.form>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -71,7 +71,7 @@ defmodule PlausibleWeb.Live.Sites do
You don't have any sites yet.
</p>
<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
</a>
</div>

View File

@ -8,75 +8,59 @@ defmodule PlausibleWeb.Live.Verification do
use Phoenix.HTML
alias Plausible.Verification.{Checks, State}
alias PlausibleWeb.Live.Components.Modal
@component PlausibleWeb.Live.Components.Verification
@slowdown_for_frequent_checking :timer.seconds(5)
def mount(
:not_mounted_at_router,
%{"domain" => domain} = session,
%{"website" => domain} = params,
_session,
socket
) 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 =
assign(socket,
site: site,
domain: domain,
modal?: !!session["modal?"],
has_pageviews?: has_pageviews?(site),
component: @component,
report_to: session["report_to"] || self(),
delay: session["slowdown"] || 500,
slowdown: session["slowdown"] || 500,
flow: session["flow"] || "",
installation_type: params["installation_type"],
report_to: self(),
delay: private[:delay] || 500,
slowdown: private[:slowdown] || 500,
flow: params["flow"] || "",
checks_pid: nil,
attempts: 0
)
if connected?(socket) and !session["modal?"] do
if connected?(socket) do
launch_delayed(socket)
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}
end
def render(assigns) do
~H"""
<div :if={@modal?} phx-click-away="reset">
<.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>
<PlausibleWeb.Components.FlowProgress.render flow={@flow} current_step="Verify installation" />
<.live_component
:if={!@modal?}
module={@component}
installation_type={@installation_type}
domain={@domain}
id="verification-standalone"
attempts={@attempts}
flow={@flow}
awaiting_first_pageview?={not @has_pageviews?}
/>
"""
end
@ -127,6 +111,10 @@ defmodule PlausibleWeb.Live.Verification do
def handle_info({:verification_end, %State{} = state}, socket) do
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,
finished?: true,
success?: interpretation.ok?,
@ -136,6 +124,20 @@ defmodule PlausibleWeb.Live.Verification do
{:noreply, assign(socket, checks_pid: nil)}
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
update_component(socket,
message: "We're visiting your site to ensure that everything is working",
@ -147,20 +149,18 @@ defmodule PlausibleWeb.Live.Verification do
socket
end
defp update_component(socket, updates) do
defp update_component(_socket, updates) do
send_update(
@component,
Keyword.merge(updates,
id:
if(socket.assigns.modal?,
do: "verification-within-modal",
else: "verification-standalone"
)
)
Keyword.merge(updates, id: "verification-standalone")
)
end
defp launch_delayed(socket) do
Process.send_after(self(), {:start, socket.assigns.report_to}, socket.assigns.delay)
end
defp has_pageviews?(site) do
Plausible.Stats.Clickhouse.has_pageviews?(site)
end
end

View File

@ -26,10 +26,6 @@ defmodule PlausibleWeb.Router do
plug :protect_from_forgery
end
pipeline :focus_layout do
plug :put_root_layout, html: {PlausibleWeb.LayoutView, :focus}
end
pipeline :app_layout do
plug :put_root_layout, html: {PlausibleWeb.LayoutView, :app}
end
@ -227,7 +223,6 @@ defmodule PlausibleWeb.Router do
post "/paddle/webhook", Api.PaddleController, :webhook
get "/paddle/currency", Api.PaddleController, :currency
get "/:domain/status", Api.InternalController, :domain_status
put "/:domain/disable-feature", Api.InternalController, :disable_feature
get "/sites", Api.InternalController, :sites
@ -237,7 +232,7 @@ defmodule PlausibleWeb.Router do
pipe_through [:browser, :csrf]
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
pipe_through PlausibleWeb.Plugs.MaybeDisableRegistration
@ -325,7 +320,6 @@ defmodule PlausibleWeb.Router do
post "/sites", SiteController, :create_site
get "/sites/:website/change-domain", SiteController, :change_domain
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-private", SiteController, :make_private
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/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/general", SiteController, :settings_general
get "/:website/settings/people", SiteController, :settings_people

View File

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

View File

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

View File

@ -37,22 +37,23 @@
<% end %>
<:footer>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4">
<%= if Keyword.fetch!(Application.get_env(:plausible, :selfhost),:disable_registration) == false do %>
<li>
Don't have an account? <%= link("Register",
to: "/register",
class: "text-gray-800 dark:text-gray-50 underline"
) %> instead.
</li>
<% end %>
<li>
<.focus_list>
<:item>
<%= if Keyword.fetch!(Application.get_env(:plausible, :selfhost), :disable_registration) == false do %>
Don't have an account
<.styled_link href="/register">
Register
</.styled_link>
instead.
<% end %>
</:item>
<:item>
Forgot password?
<a href="/password/request-reset" class="underline text-gray-800 dark:text-gray-50">
<.styled_link href="/password/reset-request">
Click here
</a>
</.styled_link>
to reset it.
</li>
</ol>
</:item>
</.focus_list>
</:footer>
</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 -> %>
<h1 class="text-xl font-black dark:text-gray-100">Create new API key</h1>
<div class="my-4">
@ -20,3 +21,4 @@
</div>
<%= submit "Continue", class: "button mt-4 w-full" %>
<% 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>
<:footer>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4">
<li>
Can't access your authenticator application?
<.focus_list>
<:item>
Can't access your authenticator app?
<.styled_link href={Routes.auth_path(@conn, :verify_2fa_recovery_code_form)}>
Use recovery code
</.styled_link>
</li>
<li :if={ee?()}>
</:item>
<:item :if={ee?()}>
Lost your recovery codes?
<.styled_link href="https://plausible.io/contact">
Contact us
</.styled_link>
</li>
</ol>
</:item>
</.focus_list>
</:footer>
<%= 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,
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"
], fn f -> %>
<h2 class="text-xl font-black dark:text-gray-100">
Enter Recovery Code
</h2>
<div class="text-sm mt-2 text-gray-500 dark:text-gray-200 leading-tight">
Can't access your authenticator application? Enter a recovery code instead.
<div class="mt-6">
<div>
<%= text_input(f, :recovery_code,
value: "",
autocomplete: "off",
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 class="mt-6">
<div>
<%= text_input(f, :recovery_code,
value: "",
autocomplete: "off",
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-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">
<p class="text-sm">
Authenticator application working again?
<a href={Routes.auth_path(@conn, :verify_2fa)} class="underline text-indigo-600">
Enter verification 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 %>
</p>
</div>
<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>
<% end %>
</div>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -8,24 +8,20 @@
</:subtitle>
<:footer>
<p>
Changed your mind?
<a
href={Routes.auth_path(@conn, :user_settings) <> "#setup-2fa"}
class="underline text-indigo-600"
>
Go back to Settings
</a>
</p>
<p>
<%= form_for @conn.params, Routes.auth_path(@conn, :initiate_2fa_setup), [id: "start-over-form"], fn _f -> %>
<.focus_list>
<:item>
Changed your mind?
<.styled_link href={Routes.auth_path(@conn, :user_settings) <> "#setup-2fa"}>
Go back to Settings
</.styled_link>
</:item>
<:item>
Having trouble?
<button class="underline text-indigo-600">
<.styled_link method="post" href={Routes.auth_path(@conn, :initiate_2fa_setup)}>
Start over
</button>
<% end %>
</p>
</.styled_link>
</:item>
</.focus_list>
</:footer>
<%= 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">
<%= for log <- @queries do %>
<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[
"phoenix_action"
] %> (<%= log[:query_duration_ms] %>ms)
@ -18,7 +18,7 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
</svg>
</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>
<%= for {key, value} <- log do %>
<tr class="table-row">

View File

@ -4,5 +4,5 @@ We've recorded <%= @current_visitors %> visitors on <%= link(@site.domain, to: "
<br /><br />
View dashboard: <%= link(@dashboard_link, to: @dashboard_link) %>
<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 %>

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.
<br /><br />
<% 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 />
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 />

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 -> %>
<h2 class="text-xl font-black dark:text-gray-100">Import from Google Analytics</h2>
<PlausibleWeb.Components.Generic.focus_box>
<:title>
Import from Google Analytics
</:title>
<%= 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-6 text-sm text-gray-500 dark:text-gray-200">
<:subtitle>
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">
<%= styled_label(f, :property, "Google Analytics property") %>
<%= styled_select(f, :property, @properties,
prompt: "(Choose property)",
required: "true"
) %>
<%= styled_error(@conn.assigns[:selected_property_error]) %>
</div>
<div class="mt-3">
<%= styled_label(f, :property, "Google Analytics property") %>
<%= styled_select(f, :property, @properties,
prompt: "(Choose property)",
required: "true"
) %>
<%= styled_error(@conn.assigns[:selected_property_error]) %>
</div>
<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">
<a
href={Routes.site_path(@conn, :settings_imports_exports, @site.domain)}
class="underline text-indigo-600"
>
Go back
</a>
</p>
<div class="mt-6 flex flex-col-reverse sm:flex-row justify-between items-center">
<p class="mt-4 sm:mt-0 dark:text-gray-100">
<a
href={Routes.site_path(@conn, :settings_imports_exports, @site.domain)}
class="underline text-indigo-600"
>
Go back
</a>
</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 %>
<span class="label-enabled pointer-events-none">
Continue ->
</span>
<%= 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">
Continue
</span>
<span class="label-disabled">
<PlausibleWeb.Components.Generic.spinner class="inline-block h-5 w-5 mr-2 text-white dark:text-gray-400" />
Checking...
</span>
<% end %>
</div>
<% end %>
<span class="label-disabled">
<PlausibleWeb.Components.Generic.spinner class="inline-block h-5 w-5 mr-2 text-white dark:text-gray-400" />
Checking...
</span>
<% end %>
</div>
<% 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>
<:footer>
<ol class="list-disc space-y-1 ml-4 mt-1 mb-4">
<li>
<.focus_list>
<:item>
<.styled_link href={Routes.auth_path(@conn, :login)}>
Login
</.styled_link>
</li>
<li>
</:item>
<:item>
<.styled_link href={Routes.auth_path(@conn, :register_form)}>
Register
</.styled_link>
</li>
<li>
</:item>
<:item>
<.styled_link href="https://plausible.io/docs">
Guides & Docs
</.styled_link>
</li>
<li>
</:item>
<:item>
<.styled_link href="https://twitter.com/plausiblehq">
Follow on Twitter
</.styled_link>
</li>
</ol>
</:item>
</.focus_list>
</:footer>
</PlausibleWeb.Components.Generic.focus_box>

View File

@ -1,44 +1,50 @@
<div class="w-full max-w-3xl mt-4 mx-auto flex">
<%= 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 -> %>
<h2 class="text-xl font-black dark:text-gray-100">Change your website domain</h2>
<PlausibleWeb.Components.FlowProgress.render
flow={PlausibleWeb.Flows.domain_change()}
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">
<%= label(f, :domain, class: "block text-sm font-medium text-gray-700 dark:text-gray-300") %>
<p class="text-gray-500 dark:text-gray-400 text-xs mt-1">
Just the naked domain or subdomain without 'www'
<%= label(f, :domain, class: "block font-medium dark:text-gray-300") %>
<p class="text-gray-500 dark:text-gray-400 mt-1 text-sm">
Just the naked domain or subdomain without 'www', 'https' etc.
</p>
<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,
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"
) %>
</div>
<%= error_tag(f, :domain) %>
</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">
Change Domain and add new Snippet
Change Domain and add new Snippet
</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 %>
</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 -> %>
<h2 class="text-xl font-black dark:text-gray-100 mb-4">Invite member to <%= @site.domain %></h2>
<PlausibleWeb.Components.Generic.focus_box>
<:title>
Invite member to <%= @site.domain %>
</:title>
<PlausibleWeb.Components.Billing.Notice.limit_exceeded
:if={Map.get(assigns, :is_at_limit, false)}
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">
<:subtitle>
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 />
The invitation will expire in 48 hours
</p>
</:subtitle>
<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-500 dark: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 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"
<%= form_for @conn, Routes.membership_path(@conn, :invite_member, @site.domain), fn f -> %>
<PlausibleWeb.Components.Billing.Notice.limit_exceeded
:if={Map.get(assigns, :is_at_limit, false)}
current_user={@current_user}
billable_user={@site.owner}
limit={Map.get(assigns, :team_member_limit, 0)}
resource="team members"
/>
<div class="my-6">
<%= label(f, :email, "Email address",
class: "block font-medium text-gray-700 dark:text-gray-300"
) %>
</div>
<%= error_tag(f, :email) %>
<%= if @conn.assigns[:error] do %>
<div class="text-red-500 text-xs mb-4"><%= @conn.assigns[:error] %></div>
<% end %>
</div>
<fieldset x-data="{selectedOption: null}">
<%= label(f, :role, class: "block text-sm font-medium text-gray-700 dark:text-gray-300") %>
<div class="mt-1 bg-white rounded-md -space-y-px dark:bg-gray-800">
<label
class="border-gray-200 dark:border-gray-500 rounded-tl-md rounded-tr-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 === 'admin', 'border-gray-200': selectedOption !== 'admin'}"
>
<%= radio_button(f, :role, "admin",
<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-500 dark: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:
"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",
"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",
placeholder: "john.doe@example.com",
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 === '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>
</div>
<%= error_tag(f, :email) %>
<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 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>
<%= if @conn.assigns[:error] do %>
<div class="text-red-500 mb-4"><%= @conn.assigns[:error] %></div>
<% end %>
</div>
</fieldset>
<div class="mt-6">
<%= submit(class: "button w-full") do %>
<svg
class="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<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">
</path>
</svg>
<span>Invite</span>
<% end %>
</div>
<% end %>
<fieldset x-data="{selectedOption: null}">
<%= label(f, :role, class: "block font-medium text-gray-700 dark:text-gray-300") %>
<div class="mt-1 bg-white rounded-md -space-y-px dark:bg-gray-800">
<label
class="border-gray-200 dark:border-gray-500 rounded-tl-md rounded-tr-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 === 'admin', 'border-gray-200': selectedOption !== 'admin'}"
>
<%= radio_button(f, :role, "admin",
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 === '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">
<%= label(f, :domain, class: "block font-medium dark:text-gray-300") %>
<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>
<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,
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",

View File

@ -3,7 +3,7 @@
<header class="relative">
<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">
Moving your Site to a different Domain? We got you!
Moving your site to a different domain? We got you!
</p>
<PlausibleWeb.Components.Generic.docs_info slug="change-domain-name" />
@ -39,7 +39,7 @@
Site Timezone
</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Update your reporting Timezone.
Update your reporting timezone.
</p>
<PlausibleWeb.Components.Generic.docs_info slug="general" />
@ -62,63 +62,28 @@
</div>
<% 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 -> %>
<header class="relative">
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
<a id="snippet">JavaScript Snippet</a>
</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Include this Snippet in the <code>&lt;head&gt;</code> of your Website.
</p>
<div class="shadow sm:rounded-md sm:overflow-hidden">
<div class="bg-white dark:bg-gray-800 py-6 px-4 space-y-6 sm:p-6">
<header class="relative">
<h2 class="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
<a id="snippet">Site Installation</a>
</h2>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-200">
Control what data is collected and verify your installation.
</p>
<PlausibleWeb.Components.Generic.docs_info slug="plausible-script" />
</header>
<PlausibleWeb.Components.Generic.docs_info slug="plausible-script" />
</header>
<div class="my-4">
<div class="relative">
<code>
<%= textarea(f, :domain,
id: "snippet_code",
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]
<div class="my-4">
<PlausibleWeb.Components.Generic.button_link
class="mt-4"
href={
Routes.site_path(@conn, :installation, @site.domain, flow: PlausibleWeb.Flows.review())
}
) %>
>
Review Installation
</PlausibleWeb.Components.Generic.button_link>
</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
site = Repo.preload(site, :members)
{dashboard_link, verification_link} =
{dashboard_link, installation_link} =
if Enum.any?(site.members, &(&1.email == recipient)) do
{
Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, []),
Routes.site_url(PlausibleWeb.Endpoint, :settings_general, site.domain,
launch_verification: true
) <> "#snippet"
Routes.site_url(PlausibleWeb.Endpoint, :installation, site.domain,
flow: PlausibleWeb.Flows.review()
)
}
else
{nil, nil}
@ -117,7 +117,7 @@ defmodule Plausible.Workers.TrafficChangeNotifier do
site,
current_visitors,
dashboard_link,
verification_link
installation_link
)
Plausible.Mailer.send(template)

View File

@ -544,7 +544,7 @@ defmodule Plausible.Verification.ChecksTest do
</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 ->
conn
|> 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
rendered =
render_component(&FlowProgress.render/1,
flow: "register",
flow: PlausibleWeb.Flows.register(),
current_step: "Register"
)
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
test "invitation" do
rendered =
render_component(&FlowProgress.render/1,
flow: "invitation",
flow: PlausibleWeb.Flows.invitation(),
current_step: "Register"
)
@ -49,11 +49,33 @@ defmodule PlausibleWeb.Components.FlowProgressTest do
test "provisioning" do
rendered =
render_component(&FlowProgress.render/1,
flow: "provisioning",
flow: PlausibleWeb.Flows.provisioning(),
current_step: "Add site info"
)
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

View File

@ -2,41 +2,6 @@ defmodule PlausibleWeb.Api.InternalControllerTest do
use PlausibleWeb.ConnCase, async: true
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
setup [:create_user, :log_in]

View File

@ -1504,8 +1504,10 @@ defmodule PlausibleWeb.AuthControllerTest do
assert element_exists?(html, "input[name=code]")
assert text_of_attr(html, "form#start-over-form", "action") ==
Routes.auth_path(conn, :initiate_2fa_setup)
assert element_exists?(
html,
~s|a[data-method="post"][data-to="#{Routes.auth_path(conn, :initiate_2fa_setup)}"|
)
end
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) ==
"/#{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.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")
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
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")
end
end
@ -457,17 +457,20 @@ defmodule PlausibleWeb.SiteControllerTest do
})
assert redirected_to(conn) ==
"/example.com/snippet?site_created=true&flow="
"/example.com/installation?site_created=true&flow="
end
end
describe "GET /:website/snippet" do
describe "GET /:website/installation" do
setup [:create_user, :log_in, :create_site]
test "shows snippet", %{conn: conn, site: site} do
conn = get(conn, "/#{site.domain}/snippet")
test "static render - spinner determining installation type", %{
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
@ -482,7 +485,7 @@ defmodule PlausibleWeb.SiteControllerTest do
assert resp =~ "Site Timezone"
assert resp =~ "Site Domain"
assert resp =~ "JavaScript Snippet"
assert resp =~ "Site Installation"
end
end
@ -1596,8 +1599,8 @@ defmodule PlausibleWeb.SiteControllerTest do
resp = html_response(conn, 200)
assert resp =~ Routes.site_path(conn, :change_domain_submit, site.domain)
assert resp =~
"Once you change your domain, you must update the JavaScript snippet on your site within 72 hours"
assert text(resp) =~
"Once you change your domain, you must update Plausible Installation on your site within 72 hours"
end
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)
end
test "domain change successful form submission redirects to snippet change info", %{
test "domain change successful form submission redirects to installation", %{
conn: conn,
site: site
} do
@ -1658,31 +1661,14 @@ defmodule PlausibleWeb.SiteControllerTest do
})
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)
assert site.domain == new_domain
assert site.domain_changed_from == original_domain
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
describe "reset stats" do

View File

@ -53,11 +53,24 @@ defmodule PlausibleWeb.StatsControllerTest do
assert text_of_element(resp, "title") == "Plausible Analytics: Live Demo"
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)
conn = get(conn, "/some-other-public-site.io")
assert html_response(conn, 200) =~ "Need to see the snippet again?"
conn = get(conn, conn |> get("/some-other-public-site.io") |> redirected_to())
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
test "can not view stats of a private website", %{conn: conn} do
@ -81,7 +94,7 @@ defmodule PlausibleWeb.StatsControllerTest do
conn: conn,
site: site
} 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"
resp = conn |> get("/" <> site.domain <> "?skip_to_dashboard=true") |> html_response(200)
@ -101,7 +114,7 @@ defmodule PlausibleWeb.StatsControllerTest do
end
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}"
end
end
@ -118,11 +131,11 @@ defmodule PlausibleWeb.StatsControllerTest do
assert html_response(conn, 200) =~ "stats-react-container"
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)
conn = get(conn, "/" <> site.domain)
assert html_response(conn, 200) =~ "Need to see the snippet again?"
conn = get(conn, conn |> get("/" <> site.domain) |> redirected_to())
assert html_response(conn, 200) =~ "Verifying your installation"
end
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)
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)
site = insert(:site, locked: true, members: [user])
conn = get(conn, "/" <> site.domain)
assert html_response(conn, 200) =~ "Need to see the snippet again?"
assert html_response(conn, 200) =~ "This dashboard is actually locked"
conn = get(conn, conn |> get("/#{site.domain}") |> redirected_to())
assert html_response(conn, 200) =~ "Verifying your installation"
end
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
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}"
end
end

View File

@ -18,8 +18,6 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
assert text_of_element(html, @progress) ==
"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)
refute class_of_element(html, @pulsating_circle) =~ "hidden"
refute element_exists?(html, @recommendations)
@ -55,11 +53,10 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
]
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 =
render_component(@component,
domain: "example.com",
modal?: true,
success?: true,
finished?: true
)
@ -77,11 +74,29 @@ defmodule PlausibleWeb.Live.Components.VerificationTest do
@tag :ee_only
test "renders contact link on >3 attempts" do
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"]|)
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"]|)
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

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]
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)
refute resp =~ "Plugin Tokens"

View File

@ -6,46 +6,33 @@ defmodule PlausibleWeb.Live.VerificationTest do
setup [:create_user, :log_in, :create_site]
@verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]|
@verification_modal ~s|div#verification-modal|
# @verify_button ~s|button#launch-verification-button[phx-click="launch-verification"]|
@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|
@heading ~s|#progress-indicator h3|
describe "GET /:domain" 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) =~
"We're visiting your site to ensure that everything is working"
assert resp =~ "Verifying your integration"
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)
assert resp =~ "Verifying your installation"
end
end
describe "GET /settings/general" 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
describe "LiveView" do
test "LiveView mounts", %{conn: conn, site: site} do
stub_fetch_body(200, "")
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) =~
"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_installation()
{:ok, lv} = kick_off_live_verification_standalone(conn, site)
{:ok, lv} = kick_off_live_verification(conn, site)
assert eventually(fn ->
html = render(lv)
@ -69,14 +56,61 @@ defmodule PlausibleWeb.Live.VerificationTest do
html = render(lv)
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
test "eventually fails to verify installation", %{conn: conn, site: site} do
stub_fetch_body(200, "")
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 =
eventually(fn ->
@ -90,133 +124,39 @@ defmodule PlausibleWeb.Live.VerificationTest do
}
end)
refute element_exists?(html, @verification_modal)
assert element_exists?(html, @retry_button)
assert html =~ "Please insert the snippet into your site"
end
end
describe "LiveView: modal" do
test "LiveView mounts", %{conn: conn, site: site} do
stub_fetch_body(200, "")
stub_installation()
defp get_lv(conn, site) do
{:ok, lv, html} = conn |> no_slowdown() |> live("/#{site.domain}/verification")
{_, 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}
end
defp get_lv_modal(conn, site, query_string \\ "") do
conn = conn |> no_slowdown() |> assign(:live_module, PlausibleWeb.Live.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
}
)
defp kick_off_live_verification(conn, site) do
{:ok, lv, _html} = conn |> no_slowdown() |> no_delay() |> live("/#{site.domain}/verification")
# {:ok, lv, _} =
# live_isolated(conn, PlausibleWeb.Live.Verification,
# session: %{
# "domain" => site.domain,
# "delay" => 0,
# "slowdown" => 0
# }
# )
#
{:ok, lv}
end
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
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)
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