defmodule PlausibleWeb.Live.SiteTransferSettings do @moduledoc """ LiveView for the "Transfer site" tile in the site Danger Zone. Lets the site owner pick a destination (another team they own/admin or another Plausible account), validates the form interactively, and dispatches to the appropriate transfer action. """ use PlausibleWeb, :live_view alias Plausible.Teams alias PlausibleWeb.Live.SiteTransferSettings.Form alias PlausibleWeb.Router.Helpers, as: Routes def mount(_params, %{"domain" => domain}, socket) do user = socket.assigns.current_user site = Plausible.Sites.get_for_user!(user, domain, roles: [:owner, :admin, :super_admin]) teams = Teams.Users.teams(user, roles: [:owner, :admin]) team_options = user |> Teams.Users.teams(roles: [:owner, :admin]) |> Enum.reject(&(&1.id == site.team_id || not &1.setup_complete)) |> Enum.map(&{&1.name, &1.identifier}) show_teams? = team_options != [] show_my_team? = not is_nil(socket.assigns[:my_team]) and socket.assigns.my_team.id != site.team_id my_team_notice = cond do not is_nil(socket.assigns[:my_team]) and socket.assigns.my_team.id == site.team_id -> "The site is already in your personal sites." is_nil(socket.assigns[:my_team]) -> "You don't have an active subscription." true -> nil end initial_destination = cond do show_teams? -> :team show_my_team? -> :my_team true -> :account end socket = socket |> assign( site: site, teams: teams, team_options: team_options, show_teams?: show_teams?, show_my_team?: show_my_team?, my_team_notice: my_team_notice ) |> assign_form(%{"destination" => initial_destination}) {:ok, socket} end def render(assigns) do ~H"""
<.flash_messages flash={@flash} /> <.tile docs="transfer-ownership"> <:title>Transfer site <:subtitle>Move this site to another team or Plausible account. <.form :let={f} for={@form} id="site-transfer-form" phx-change="validate" phx-submit="save" >
<.label>Destination
<.input type="radio" id="destination-team" name={f[:destination].name} value={:team} checked={f[:destination].value == :team and @show_teams?} disabled={not @show_teams?} label="Team" />

The site will immediately move to the selected team. Billing does not transfer.

<.input type="select" field={f[:team_identifier]} options={@team_options} prompt="Select a team" mt?={false} />

You aren't a member of any other teams or you lack privileges for transfer.

<.input type="radio" id="destination-account" name={f[:destination].name} value={:account} checked={f[:destination].value == :account} label="Another Plausible account" />

The recipient will receive an email and have 48 hours to accept the transfer. You'll keep Guest Editor access by default.

<.input type="email" field={f[:email]} label="Email address" placeholder="example@email.com" mt?={false} />
<.input type="radio" id="destination-my_team" name={f[:destination].name} value={:my_team} checked={f[:destination].value == :my_team} disabled={not @show_my_team?} label="My personal sites" />
<.input :if={@show_my_team?} type="hidden" field={f[:my_team_available]} value={true} mt?={false} />

{@my_team_notice}

<.button type="submit" theme="danger" phx-disable-with="Transferring..." > {submit_label(f[:destination].value)}
""" end def handle_event("validate", %{"form" => params}, socket) do {:noreply, assign_form(socket, params)} end def handle_event("save", %{"form" => params}, socket) do changeset = Form.changeset(params) case Ecto.Changeset.apply_action(changeset, :insert) do {:ok, %Form{destination: :team, team_identifier: identifier}} -> team = Enum.find(socket.assigns.teams, &(&1.identifier == identifier)) do_change_team(socket, team, params) {:ok, %Form{destination: :my_team}} -> do_change_team(socket, socket.assigns[:my_team], params) {:ok, %Form{destination: :account, email: email}} -> do_transfer_ownership(socket, email, params) {:error, changeset} -> {:noreply, assign(socket, form: to_form(changeset, as: :form))} end end defp do_change_team(socket, destination_team, params) do if destination_team do my_team? = not is_nil(socket.assigns[:my_team]) and socket.assigns.my_team.id == destination_team.id user = socket.assigns.current_user site = socket.assigns.site error_field = if(my_team?, do: :my_team_available, else: :team_identifier) case Teams.Sites.Transfer.change_team(site, user, destination_team) do :ok -> {:noreply, socket |> put_flash(:success, "Site team was changed") |> redirect(to: Routes.site_path(socket, :index, __team: destination_team.identifier))} {:error, reason} -> {:noreply, assign_form(socket, params, action: :insert, field_errors: [{error_field, change_team_error_message(reason, my_team?)}] )} end else {:noreply, assign_form(socket, params, action: :insert, field_errors: [{:team_identifier, "Please select a team"}] )} end end defp do_transfer_ownership(socket, email, params) do user = socket.assigns.current_user site = socket.assigns.site case Teams.Invitations.InviteToSite.invite(site, user, email, :owner) do {:ok, _invitation} -> {:noreply, socket |> put_flash(:success, "Site transfer request has been sent to #{email}") |> redirect(to: Routes.site_path(socket, :settings_people, site.domain))} {:error, %Ecto.Changeset{} = changeset} -> message = case Plausible.ChangesetHelpers.traverse_errors(changeset) do %{invitation: ["already sent" | _]} -> "Invitation has already been sent" _ -> "Site transfer request to #{email} has failed" end {:noreply, assign_form(socket, params, action: :insert, field_errors: [{:email, message}] )} end end defp assign_form(socket, params, opts \\ []) do changeset = Form.changeset(params) changeset = Enum.reduce(Keyword.get(opts, :field_errors, []), changeset, fn {field, message}, cs -> Ecto.Changeset.add_error(cs, field, message) end) changeset = case opts[:action] do nil -> changeset action -> Map.put(changeset, :action, action) end assign(socket, form: to_form(changeset, as: :form)) end defp submit_label(:team), do: "Move site" defp submit_label(:my_team), do: "Move site" defp submit_label(_), do: "Send transfer request" defp change_team_error_message(:no_plan, false = _my_team?) do "This team doesn't have a subscription. Please start a subscription for the team first and then try moving the site again." end defp change_team_error_message(:no_plan, true = _my_team?) do "You don't have a subscription. Please start a subscription first and then try moving the site again." end defp change_team_error_message({:over_plan_limits, _}, false = _my_team?) do "This site's usage exceeds the destination team's subscription limits. Upgrade the team's subscription to continue." end defp change_team_error_message({:over_plan_limits, _}, true = _my_team?) do "This site's usage exceeds your subscription limits. Upgrade your subscription to continue." end defp change_team_error_message(_, false = _my_team?) do "Sorry, this team cannot be used." end defp change_team_error_message(_, true = _my_team?) do "Sorry, My personal sites cannot be used." end end