Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 86 additions & 3 deletions e2e/tests/dashboard/team-setup.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { test, expect } from '@playwright/test'
import { setupSite, subscribeToPlan } from '../fixtures'
import { expectLiveViewConnected } from '../test-utils'
import { test, expect, Page } from '@playwright/test'
import {
login,
logout,
register,
setupSite,
subscribeToPlan
} from '../fixtures'
import { expectLiveViewConnected, randomID } from '../test-utils'

test('submitting team name via Enter key does not crash', async ({
page,
Expand Down Expand Up @@ -115,3 +121,80 @@ test('removing and re-adding a row', async ({ page, request }) => {
await expect(row2.locator('input[type="email"]')).toHaveValue('')
await expect(row2.getByTestId('role')).toHaveText('Viewer')
})

function newUser() {
const userID = randomID()

return {
name: `User ${userID}`,
email: `email-${userID}@example.com`,
password: 'VeryStrongVerySecret'
}
}

async function createTeamInOnboarding(page: Page, teamName: string) {
await expect(page).toHaveURL(/\/team\/setup\?flow=register/)
await expectLiveViewConnected(page)

const nameInput = page.locator('input[name="team[name]"]')
await nameInput.clear()
await nameInput.fill(teamName)

await page.getByRole('button', { name: 'Create team' }).click()
}

async function expectCurrentTeamOnSitesPage(page: Page, teamName: string) {
await page.goto('/sites', { waitUntil: 'commit' })
await expectLiveViewConnected(page)

await expect(page.getByRole('heading', { level: 2 })).toHaveText(teamName)
}

test('team created during onboarding after adding a site stays the current team', async ({
page,
request
}) => {
const user = newUser()
const teamName = `Onboarded team ${randomID()}`

await register({ page, request, user })

await page.getByLabel('Domain').fill(`${randomID()}.example.com`)
await page.getByRole('button', { name: 'Add site' }).click()

await createTeamInOnboarding(page, teamName)

await expect(page).toHaveURL(/\/installation\?flow=register&__team=/)

// without the `__team` param
await expectCurrentTeamOnSitesPage(page, teamName)

// with a fresh session
await logout(page)
await login({ page, user })
await expectCurrentTeamOnSitesPage(page, teamName)
})

test('team created during onboarding after skipping adding a site stays the current team', async ({
page,
request
}) => {
const user = newUser()
const teamName = `Onboarded team ${randomID()}`

await register({ page, request, user })

await page.getByRole('link', { name: 'Skip' }).click()

await createTeamInOnboarding(page, teamName)

await expect(page).toHaveURL(/\/sites\?__team=/)

// without the `__team` param
await expectCurrentTeamOnSitesPage(page, teamName)

// with a fresh session
await logout(page)
await login({ page, user })
await expectCurrentTeamOnSitesPage(page, teamName)
})
6 changes: 5 additions & 1 deletion e2e/tests/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export async function login({ page, user }: { page: Page; user: User }) {
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible()

await page.getByLabel('Email').fill(user.email)
await page.getByLabel('Password').fill(user.password)
await page.getByLabel('Password', { exact: true }).fill(user.password)
await page.getByRole('button', { name: 'Sign in' }).click()

await expect(page.getByRole('button', { name: user.name })).toBeVisible()
Expand Down Expand Up @@ -155,6 +155,10 @@ export async function addSite({

await page.getByRole('button', { name: 'Add site' }).click()

await expect(page).toHaveURL(/\/team\/setup/)

await page.getByRole('link', { name: 'Skip' }).click()

await expect(page).toHaveURL(/\/installation/)

await expect(
Expand Down
36 changes: 36 additions & 0 deletions lib/plausible/teams.ex
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,42 @@ defmodule Plausible.Teams do
end
end

@doc """
Creates a new team owned by the given user and invites the given members.

Unlike `get_or_create/1`, this always creates a new team, regardless of
any other teams the user is already a member or an owner of. Those teams
are left untouched.
"""
@spec create(Auth.User.t(), String.t(), [InviteToTeam.invite_entry()]) ::
{:ok, Teams.Team.t()} | {:error, any()}
def create(%Auth.User{} = user, name, invite_entries)
when is_binary(name) and is_list(invite_entries) do
with :ok <- check_user_type(user),
{:ok, {team, invitations}} <- do_create(user, name, invite_entries) do
InviteToTeam.send_invitation_emails(invitations)
{:ok, team}
end
end

defp do_create(user, name, invite_entries) do
Repo.transact(fn ->
with {:ok, team} <- insert_setup_team(name),
{:ok, _} <- Repo.insert(Teams.Membership.changeset(team, user, :owner)),
{:ok, invitations} <- InviteToTeam.invite_all(team, user, invite_entries) do
{:ok, {team, invitations}}
end
end)
end

defp insert_setup_team(name) do
%Teams.Team{}
|> Teams.Team.changeset(%{name: name})
|> Teams.Team.validate_not_reserved_name()
|> Teams.Team.setup_changeset()
|> Repo.insert()
end

@spec delete(Teams.Team.t()) :: {:ok, :deleted} | {:error, :active_subscription}
def delete(team) do
team = Teams.with_subscription(team)
Expand Down
41 changes: 36 additions & 5 deletions lib/plausible/teams/team.ex
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,35 @@ defmodule Plausible.Teams.Team do

def suggested_name(_user_name), do: @suggested_name_fallback

@doc """
Builds the team name suggested from a site domain, using the label right
before the public suffix, e.g. "shop.wannabuy.co.uk/blog" -> "Wannabuy".

Falls back to `suggested_name/1` when the domain doesn't yield a name-like
label or is invalid according to the rules of `valid_suggestion?/1`.
"""
def suggested_name_from_domain(domain, user_name) when is_binary(domain) do
with [host | _] <- String.split(domain, "/", parts: 2),
registrable_domain when is_binary(registrable_domain) <-
PublicSuffix.registrable_domain(host),
[label | _] <- String.split(registrable_domain, "."),
true <- String.match?(label, ~r/\p{L}/u),
suggested = humanize_domain_label(label),
true <- String.length(suggested) <= @max_name_length and valid_suggestion?(suggested) do
suggested
else
_ -> suggested_name(user_name)
end
end

def suggested_name_from_domain(_domain, user_name), do: suggested_name(user_name)

defp humanize_domain_label(label) do
label
|> String.split(["-", "_"], trim: true)
|> Enum.map_join(" ", &String.capitalize/1)
end

defp valid_suggestion?(name) do
byte_size(name) <= @max_name_bytes and not contains_scheme?(name)
end
Expand Down Expand Up @@ -147,12 +176,14 @@ defmodule Plausible.Teams.Team do
end

def name_changeset(team, attrs \\ %{}) do
changeset =
team
|> cast(attrs, [:name])
|> validate_name()
|> validate_required(:name)
team
|> cast(attrs, [:name])
|> validate_name()
|> validate_required(:name)
|> validate_not_reserved_name()
end

def validate_not_reserved_name(changeset) do
# Do not allow an actual team keep Plausible.Teams.default_name()
# as its name. This is just to avoid confusion in the UI and keep
# the auto-created team distinguishable from a real one.
Expand Down
14 changes: 14 additions & 0 deletions lib/plausible_web/components/generic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,20 @@ defmodule PlausibleWeb.Components.Generic do
"""
end

attr :heading, :string, required: true
attr :subtitle, :string, required: true

def onboarding_page_heading(assigns) do
~H"""
<div class="w-full max-w-md mx-auto mt-10 sm:mt-16 px-4 flex flex-col gap-y-2 dark:text-gray-300">
<h1 class="text-lg sm:text-xl font-semibold">{@heading}</h1>
<p class="text-base text-gray-500 dark:text-gray-400 text-pretty">
{@subtitle}
</p>
</div>
"""
end

alias Phoenix.LiveView.JS

slot :inner_block, required: true
Expand Down
24 changes: 8 additions & 16 deletions lib/plausible_web/components/site/new_site_form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ defmodule PlausibleWeb.Components.Site.NewSiteForm do
attr :site_limit, :integer, required: true
attr :current_user, :any, required: true
attr :current_team, :any, required: true
attr :back_button_text, :string, required: true
attr :back_button_href, :string, required: true
attr :secondary_action_text, :string, required: true
attr :secondary_action_href, :string, required: true

def new_site_form(assigns) do
~H"""
<.heading_and_subtitle heading="Add a website" subtitle="Start measuring traffic on a new site." />
<.onboarding_page_heading
heading="Add a website"
subtitle="Start measuring traffic on a new site."
/>
<div class="w-full max-w-md mx-auto mt-10 pb-16 px-4 dark:text-gray-300">
<.form :let={f} class="flex flex-col gap-y-8" for={@changeset} action={@form_submit_url}>
<PlausibleWeb.Components.Billing.Notice.limit_exceeded
Expand Down Expand Up @@ -46,8 +49,8 @@ defmodule PlausibleWeb.Components.Site.NewSiteForm do
</script>

<div class="flex justify-end items-center gap-x-2">
<.button_link theme="ghost" href={@back_button_href}>
{@back_button_text}
<.button_link theme="ghost" href={@secondary_action_href}>
{@secondary_action_text}
</.button_link>
<.button
disabled={@site_limit_exceeded?}
Expand All @@ -61,15 +64,4 @@ defmodule PlausibleWeb.Components.Site.NewSiteForm do
</div>
"""
end

def heading_and_subtitle(assigns) do
~H"""
<div class="w-full max-w-md mx-auto mt-10 sm:mt-16 px-4 flex flex-col gap-y-2 dark:text-gray-300">
<h1 class="text-lg sm:text-xl font-semibold">{@heading}</h1>
<p class="text-base text-gray-500 dark:text-gray-400 text-pretty">
{@subtitle}
</p>
</div>
"""
end
end
7 changes: 5 additions & 2 deletions lib/plausible_web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ defmodule PlausibleWeb.AuthController do
end

defp do_activate(conn, user, code) do
has_any_invitations? = Plausible.Teams.Users.has_sites?(user, include_pending?: true)
has_any_invitations? =
Plausible.Teams.Users.has_sites?(user, include_pending?: true) or
Teams.Invitations.pending_team_invitations_for(user) != []

has_any_memberships? = Plausible.Teams.Users.has_sites?(user, include_pending?: false)

flow = conn.params["flow"]
Expand Down Expand Up @@ -301,7 +304,7 @@ defmodule PlausibleWeb.AuthController do
accept_team_invitation(conn, params["team_identifier"], user)

params["register_action"] == "register_form" ->
~p"/sites/new"
~p"/sites/new?#{[flow: PlausibleWeb.Flows.register()]}"

true ->
params["return_to"]
Expand Down
12 changes: 9 additions & 3 deletions lib/plausible_web/controllers/site_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ defmodule PlausibleWeb.SiteController do
|> Plausible.Mailer.send()
end

redirect(conn,
to: ~p"/#{site.domain}/installation?#{[flow: flow]}"
)
redirect(conn, to: site_created_redirect_path(site, flow))

{:error, _, :permission_denied, _} ->
conn
Expand All @@ -68,6 +66,14 @@ defmodule PlausibleWeb.SiteController do
end
end

defp site_created_redirect_path(site, flow) do
if flow == PlausibleWeb.Flows.register() do
~p"/team/setup?#{[flow: flow, domain: site.domain]}"
else
~p"/#{site.domain}/installation?#{[flow: flow]}"
end
end

defp render_new_site_form(conn, flow, opts \\ []) do
defaults = [
changeset: Plausible.Site.changeset(%Plausible.Site{}),
Expand Down
3 changes: 2 additions & 1 deletion lib/plausible_web/flows.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ defmodule PlausibleWeb.Flows do
@doc """
The steps shown in the onboarding flow's progress indicator.
"""
def onboarding_steps, do: [add_site_step(), installation_step()]
def onboarding_steps, do: [add_site_step(), team_setup_step(), installation_step()]

def add_site_step, do: "Add site info"
def team_setup_step, do: "Set up team"
def installation_step, do: "Install Plausible"
end
2 changes: 1 addition & 1 deletion lib/plausible_web/live/installation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ defmodule PlausibleWeb.Live.Installation do

~H"""
<.onboarding_or_app_layout {assigns}>
<PlausibleWeb.Components.Site.NewSiteForm.heading_and_subtitle
<.onboarding_page_heading
heading={@heading}
subtitle={@subtitle}
/>
Expand Down
Loading
Loading