lib/ash_authentication_phoenix/components/password.ex

defmodule AshAuthentication.Phoenix.Components.Password do
  use AshAuthentication.Phoenix.Overrides.Overridable,
    root_class: "CSS class for the root `div` element.",
    hide_class: "CSS class to apply to hide an element.",
    show_first: "The form to show on first load.  Either `:sign_in` or `:register`.",
    interstitial_class: "CSS class for the `div` element between the form and the button.",
    sign_in_toggle_text:
      "Toggle text to display when the sign in form is not showing (or `nil` to disable).",
    register_toggle_text:
      "Toggle text to display when the register form is not showing (or `nil` to disable).",
    reset_toggle_text:
      "Toggle text to display when the reset form is not showing (or `nil` to disable).",
    toggler_class: "CSS class for the toggler `a` element.",
    slot_class: "CSS class for the `div` surrounding the slot."

  @moduledoc """
  Generates sign in, registration and reset forms for a resource.

  ## Component hierarchy

  This is the top-most strategy-specific component, nested below
  `AshAuthentication.Phoenix.Components.SignIn`.

  Children:

    * `AshAuthentication.Phoenix.Components.Password.SignInForm`
    * `AshAuthentication.Phoenix.Components.Password.RegisterForm`
    * `AshAuthentication.Phoenix.Components.Password.ResetForm`

  ## Props

    * `strategy` - The strategy configuration as per
      `AshAuthentication.Info.strategy/2`.  Required.
    * `overrides` - A list of override modules.
    * `show_first` - either `:sign_in`, `:register` or `:reset` which controls
      which form is visible on first load.

  ## Slots

    * `sign_in_extra` - rendered inside the sign-in form with the form passed as
      a slot argument.
    * `register_extra` - rendered inside the registration form with the form
      passed as a slot argument.
    * `reset_extra` - rendered inside the reset form with the form passed as a
      slot argument.

  ```heex
  <.live_component
    module={#{inspect(__MODULE__)}}
    strategy={AshAuthentication.Info.strategy!(Example.User, :password)}
    id="user-with-password"
    socket={@socket}
    overrides={[AshAuthentication.Phoenix.Overrides.Default]}>

    <:sign_in_extra :let={form}>
      <.input field={form[:capcha]} />
    </:sign_in_extra>

    <:register_extra :let={form}>
      <.input field={form[:name]} />
    </:register_extra>

    <:reset_extra :let={form}>
      <.input field={form[:capcha]} />
    </:reset_extra>
  </.live_component>
  ```



  #{AshAuthentication.Phoenix.Overrides.Overridable.generate_docs()}
  """

  use Phoenix.LiveComponent
  alias AshAuthentication.{Info, Phoenix.Components.Password, Strategy}
  alias Phoenix.LiveView.{JS, Rendered, Socket}
  import Slug

  @type props :: %{
          required(:strategy) => AshAuthentication.Strategy.t(),
          optional(:overrides) => [module]
        }

  slot :sign_in_extra
  slot :register_extra
  slot :reset_extra

  @doc false
  @impl true
  @spec render(props) :: Rendered.t() | no_return
  def render(assigns) do
    strategy = assigns.strategy

    subject_name =
      assigns.strategy.resource
      |> Info.authentication_get_by_subject_action_name!()
      |> to_string()
      |> slugify()

    strategy_name =
      assigns.strategy
      |> Strategy.name()
      |> to_string()
      |> slugify()

    register_enabled? =
      strategy.registration_enabled? && override_for(assigns.overrides, :register_toggle_text)

    reset_enabled? =
      strategy.resettable && override_for(assigns.overrides, :reset_toggle_text)

    reset_id =
      strategy.resettable &&
        generate_id(
          subject_name,
          strategy_name,
          strategy.resettable.request_password_reset_action_name
        )

    assigns =
      assigns
      |> assign(
        :sign_in_id,
        generate_id(subject_name, strategy_name, strategy.sign_in_action_name)
      )
      |> assign(
        :register_id,
        generate_id(subject_name, strategy_name, strategy.register_action_name)
      )
      |> assign_new(:show_first, fn -> override_for(assigns.overrides, :show_first, :sign_in) end)
      |> assign(:hide_class, override_for(assigns.overrides, :hide_class))
      |> assign(:reset_enabled?, reset_enabled?)
      |> assign(:register_enabled?, register_enabled?)
      |> assign(:sign_in_enabled?, !is_nil(override_for(assigns.overrides, :sign_in_toggle_text)))
      |> assign(:reset_id, reset_id)
      |> assign_new(:overrides, fn -> [AshAuthentication.Phoenix.Overrides.Default] end)

    ~H"""
    <div class={override_for(@overrides, :root_class)}>
      <div id={"#{@sign_in_id}-wrapper"} class={unless @show_first == :sign_in, do: @hide_class}>
        <.live_component
          :let={form}
          module={Password.SignInForm}
          id={@sign_in_id}
          strategy={@strategy}
          label={false}
          overrides={@overrides}
        >
          <%= if @sign_in_extra do %>
            <div class={override_for(@overrides, :slot_class)}>
              <%= render_slot(@sign_in_extra, form) %>
            </div>
          <% end %>

          <div class={override_for(@overrides, :interstitial_class)}>
            <%= if @reset_enabled? do %>
              <.toggler
                show={@reset_id}
                hide={[@sign_in_id, @register_id]}
                message={override_for(@overrides, :reset_toggle_text)}
                overrides={@overrides}
              />
            <% end %>

            <%= if @register_enabled? do %>
              <.toggler
                show={@register_id}
                hide={[@sign_in_id, @reset_id]}
                message={override_for(@overrides, :register_toggle_text)}
                overrides={@overrides}
              />
            <% end %>
          </div>
        </.live_component>
      </div>

      <%= if @register_enabled? do %>
        <div id={"#{@register_id}-wrapper"} class={unless @show_first == :register, do: @hide_class}>
          <.live_component
            :let={form}
            module={Password.RegisterForm}
            id={@register_id}
            strategy={@strategy}
            label={false}
            overrides={@overrides}
          >
            <%= if @register_extra do %>
              <div class={override_for(@overrides, :slot_class)}>
                <%= render_slot(@register_extra, form) %>
              </div>
            <% end %>

            <div class={override_for(@overrides, :interstitial_class)}>
              <%= if @reset_enabled? do %>
                <.toggler
                  show={@reset_id}
                  hide={[@sign_in_id, @register_id]}
                  message={override_for(@overrides, :reset_toggle_text)}
                  overrides={@overrides}
                />
              <% end %>
              <%= if @sign_in_enabled? do %>
                <.toggler
                  show={@sign_in_id}
                  hide={[@register_id, @reset_id]}
                  message={override_for(@overrides, :sign_in_toggle_text)}
                  overrides={@overrides}
                />
              <% end %>
            </div>
          </.live_component>
        </div>
      <% end %>

      <%= if @reset_enabled? do %>
        <div id={"#{@reset_id}-wrapper"} class={unless @show_first == :reset, do: @hide_class}>
          <.live_component
            :let={form}
            module={Password.ResetForm}
            id={@reset_id}
            strategy={@strategy}
            label={false}
            overrides={@overrides}
          >
            <%= if @reset_extra do %>
              <div class={override_for(@overrides, :slot_class)}>
                <%= render_slot(@reset_extra, form) %>
              </div>
            <% end %>

            <div class={override_for(@overrides, :interstitial_class)}>
              <%= if @register_enabled? do %>
                <.toggler
                  show={@register_id}
                  hide={[@sign_in_id, @reset_id]}
                  message={override_for(@overrides, :register_toggle_text)}
                  overrides={@overrides}
                />
              <% end %>
              <%= if @sign_in_enabled? do %>
                <.toggler
                  show={@sign_in_id}
                  hide={[@register_id, @reset_id]}
                  message={override_for(@overrides, :sign_in_toggle_text)}
                  overrides={@overrides}
                />
              <% end %>
            </div>
          </.live_component>
        </div>
      <% end %>
    </div>
    """
  end

  defp generate_id(subject_name, strategy_name, action) do
    action =
      action
      |> to_string()
      |> slugify()

    "#{subject_name}-#{strategy_name}-#{action}"
  end

  @doc false
  @spec toggler(Socket.assigns()) :: Rendered.t() | no_return
  def toggler(assigns) do
    ~H"""
    <a href="#" phx-click={toggle_js(@show, @hide)} class={override_for(@overrides, :toggler_class)}>
      <%= @message %>
    </a>
    """
  end

  defp toggle_js(show, hides, %JS{} = js \\ %JS{}) do
    show_wrapper = "##{show}-wrapper"

    js =
      js
      |> JS.show(to: show_wrapper)
      |> JS.focus_first(to: show_wrapper)

    hides
    |> Enum.reject(&is_nil/1)
    |> Enum.reduce(js, fn hide, js ->
      JS.hide(js, to: "##{hide}-wrapper")
    end)
  end
end