defmodule PlausibleWeb.Live.RegisterForm do @moduledoc """ LiveView for registration form. """ use PlausibleWeb, :live_view alias Plausible.Auth alias Plausible.Repo alias Plausible.Teams def mount(params, _session, socket) do socket = socket |> assign_new(:invitation, fn -> if invitation_id = params["invitation_id"] do find_by_id_unified(invitation_id) end end) |> assign_new(:team_identifier, fn %{invitation: invitation} -> if invitation do invitation.team_identifier end end) if socket.assigns.live_action == :register_from_invitation_form and socket.assigns.invitation == nil do {:ok, assign(socket, invitation_expired: true, heading: "Invitation no longer valid", subtitle: "This invitation has expired or was revoked. Ask your team admin to send you a new invitation." )} else changeset = if invitation = socket.assigns.invitation do Auth.User.settings_changeset(%Auth.User{email: invitation.email}) else Auth.User.settings_changeset(%Auth.User{}) end {heading, subtitle} = heading_and_subtitle(socket.assigns.live_action) {:ok, assign(socket, form: to_form(changeset), captcha_error: nil, password_strength: Auth.User.password_strength(changeset), disable_submit: false, trigger_submit: false, heading: heading, subtitle: subtitle )} end end defp heading_and_subtitle(:register_from_invitation_form) do {"Create your account", "Accept your invitation to join your team."} end defp heading_and_subtitle(:register_form) do if ce?() do {"Create your #{Plausible.product_name()} account", "Start tracking privacy-friendly analytics in minutes."} else {"Start your 30-day free trial", "No credit card required. Cancel anytime."} end end def render(%{invitation_expired: true} = assigns) do ~H""" <.auth_container class="flex gap-3 justify-center"> <.button_link href="/register" mt?={false}> Start free trial <.button_link href="/login" theme="secondary" mt?={false}> Sign in """ end def render(assigns) do ~H""" <.auth_container> <.form :let={f} for={@form} id="register-form" class="flex flex-col gap-y-6" action={Routes.auth_path(@socket, :login)} phx-hook="Metrics" phx-change="validate" phx-submit="register" phx-trigger-action={@trigger_submit} > <%= if @invitation do %> <.email_input field={f[:email]} for_invitation={true} /> <.name_input field={f[:name]} /> <% else %> <.name_input field={f[:name]} /> <.email_input field={f[:email]} for_invitation={false} /> <% end %>
<.password_input_with_strength field={f[:password]} strength={@password_strength} phx-debounce={200} mt?={false} />
<.password_length_hint minimum={12} field={f[:password]} hide_when_used?={true} />
<%= if PlausibleWeb.Captcha.enabled?() do %>

{@captcha_error}

<% end %>
<% submit_text = if ce?() or @invitation do "Create my account" else "Start my free trial" end %> <.button id="register" disabled={@disable_submit} type="submit" class="w-full" mt?={false}> {submit_text}

Already have an account? <.styled_link href="/login"> Sign in

""" end defp name_input(assigns) do ~H"""
<.input field={@field} placeholder="Jane Doe" phx-debounce={200} mt?={false} autofocus="autofocus" />
""" end defp email_input(assigns) do email_readonly = if assigns[:for_invitation] do [readonly: "readonly"] else [] end assigns = assign(assigns, :email_readonly, email_readonly) ~H"""
<.input type="email" field={@field} placeholder="example@email.com" phx-debounce={200} mt?={false} {@email_readonly} />
""" end def handle_event("validate", %{"user" => params}, socket) do changeset = params |> Auth.User.new() |> Map.put(:action, :validate) password_strength = Auth.User.password_strength(changeset) {:noreply, assign(socket, form: to_form(changeset), password_strength: password_strength, captcha_error: nil )} end def handle_event( "register", %{"user" => _} = params, %{assigns: %{invitation: %{} = invitation}} = socket ) do if not PlausibleWeb.Captcha.enabled?() or PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do user = params["user"] |> Map.put("email", invitation.email) |> Auth.User.new() with_team? = invitation.type == :site_transfer add_user(socket, user, with_team?: with_team?) else {:noreply, assign(socket, :captcha_error, "Please complete the captcha to register")} end end def handle_event("register", %{"user" => _} = params, socket) do if not PlausibleWeb.Captcha.enabled?() or PlausibleWeb.Captcha.verify(params["h-captcha-response"]) do user = Auth.User.new(params["user"]) add_user(socket, user) else {:noreply, assign(socket, :captcha_error, "Please complete the captcha to register")} end end def handle_event("send-metrics-after", _params, socket) do {:noreply, assign(socket, trigger_submit: true)} end defp add_user(socket, user, opts \\ []) do result = Repo.transaction(fn -> do_add_user(user, opts) end) case result do {:ok, _user} -> socket = assign(socket, disable_submit: true) on_ee do event_name = "Signup#{if socket.assigns.invitation, do: " via invitation"}" {:noreply, push_event(socket, "send-metrics", %{event_name: event_name})} else {:noreply, assign(socket, trigger_submit: true)} end {:error, changeset} -> {:noreply, assign(socket, form: to_form(Map.put(changeset, :action, :validate)) )} end end defp do_add_user(user, opts) do case Repo.insert(user) do {:ok, user} -> if opts[:with_team?] do {:ok, _} = Plausible.Teams.get_or_create(user) end user {:error, reason} -> Repo.rollback(reason) end end defp find_by_id_unified(invitation_or_transfer_id) do result = with {:error, :invitation_not_found} <- find_team_invitation_by_id_unified(invitation_or_transfer_id), {:error, :invitation_not_found} <- find_invitation_by_id_unified(invitation_or_transfer_id) do find_transfer_by_id_unified(invitation_or_transfer_id) end case result do {:error, :invitation_not_found} -> nil {:ok, unified} -> unified end end defp find_team_invitation_by_id_unified(id) do invitation = Teams.Invitation |> Repo.get_by(invitation_id: id) |> Repo.preload(:team) case invitation do nil -> {:error, :invitation_not_found} team_invitation -> {:ok, %{ type: :team_invitation, email: team_invitation.email, team_identifier: team_invitation.team.identifier }} end end defp find_invitation_by_id_unified(id) do invitation = Teams.GuestInvitation |> Repo.get_by(invitation_id: id) |> Repo.preload(:team_invitation) case invitation do nil -> {:error, :invitation_not_found} guest_invitation -> {:ok, %{ type: :guest_invitation, email: guest_invitation.team_invitation.email, team_identifier: nil }} end end defp find_transfer_by_id_unified(id) do transfer = Teams.SiteTransfer |> Repo.get_by(transfer_id: id) case transfer do nil -> {:error, :invitation_not_found} transfer -> {:ok, %{ type: :site_transfer, email: transfer.email, team_identifier: nil }} end end end