Skip to content

LiveFlash (put_flash) doesn't work when push_navigateing across live_sessions #3686

Open
@maxpohlmann

Description

@maxpohlmann

Environment

  • Elixir version (elixir -v): 1.17.2
  • Phoenix version (mix deps): 1.7.19
  • Phoenix LiveView version (mix deps): 1.0.4
  • Operating system: MacOS 15
  • Browsers you attempted to reproduce this bug on (the more the merrier): Firefox
  • Does the problem persist after removing "assets/node_modules" and trying again? Yes/no: Yes

Actual behavior

When navigating across LiveViews in different live sessions with push_navigate, live flashes aren't being set and hence aren't being displayed on the redirected LiveView.

I take it that one isn't supposed to use push_navigate to navigate between LiveViews of different sessions anyways, since doing so yields the browser error unauthorized live_redirect. Falling back to page request as well as the server warning navigate event to [...] failed because you are redirecting across live_sessions. A full page reload will be performed instead. Furthermore, I can easily get the expected behaviour by replacing push_navigate with redirect. ...

Expected behavior

... However, since this fallback behaviour exists for this situation, I think live flashes should work properly as well, since otherwise it can be hard to figure out why they aren't being displayed. I.e. live flashes should be displayed even when using push_navigate across different live sessions.

Reproducing the bug

Here is a single-file application showing the bug:
A -> B: Using push_navigate within a live session => Flash is shown
B -> C: Using redirect across two live sessions => Flash is shown
C -> A: Using push_navigate across two live sessions => Flash is not shown

# main.exs
# Run with: elixir main.exs

Application.put_env(:sample, Example.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 5001],
  server: true,
  live_view: [signing_salt: "aaaaaaaa"],
  secret_key_base: String.duplicate("a", 64)
)

Mix.install([
  {:plug_cowboy, "~> 2.5"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7"},
  {:phoenix_live_view,
   github: "phoenixframework/phoenix_live_view", branch: "main", override: true}
])

defmodule Example.CoreComponents do
  # default core components for live flashes
  use Phoenix.Component
  alias Phoenix.LiveView.JS

  attr :id, :string, doc: "the optional id of flash container"
  attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
  attr :title, :string, default: nil
  attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
  attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"

  slot :inner_block, doc: "the optional inner block that renders the flash message"

  def flash(assigns) do
    assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)

    ~H"""
    <div
      :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
      id={@id}
      role="alert"
      class={[
        "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
        @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
        @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
      ]}
      {@rest}
    >
      <p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
        {@title}
      </p>
      <p class="mt-2 text-sm leading-5">{msg}</p>
    </div>
    """
  end

  attr :flash, :map, required: true, doc: "the map of flash messages"
  attr :id, :string, default: "flash-group", doc: "the optional id of flash container"

  def flash_group(assigns) do
    ~H"""
    <div id={@id}>
      <.flash kind={:info} title="Success!" flash={@flash} />
      <.flash kind={:error} title="Error!" flash={@flash} />
    </div>
    """
  end
end

defmodule Example.Layouts do
  use Phoenix.LiveView

  def render("app.html", assigns) do
    ~H"""
    <script src="/assets/phoenix/phoenix.js">
    </script>
    <script src="/assets/phoenix_live_view/phoenix_live_view.js">
    </script>
    <script src="https://cdn.tailwindcss.com">
    </script>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <style>
      * { font-size: 1.1em; }
    </style>
    <Example.CoreComponents.flash_group flash={@flash} />
    {@inner_content}
    """
  end
end

defmodule Example.ALive do
  use Phoenix.LiveView, layout: {Example.Layouts, :app}

  def render(assigns) do
    ~H"""
    <h1>A</h1>
    <button phx-click="go">To B</button>
    """
  end

  def handle_event("go", _unsigned_params, socket) do
    {:noreply, socket |> put_flash(:info, "Flash from A") |> push_navigate(to: "/b")}
  end
end

defmodule Example.BLive do
  use Phoenix.LiveView, layout: {Example.Layouts, :app}

  def render(assigns) do
    ~H"""
    <h1>B</h1>
    <button phx-click="go">To C</button>
    """
  end

  def handle_event("go", _unsigned_params, socket) do
    {:noreply, socket |> put_flash(:info, "Flash from B") |> redirect(to: "/c")}
  end
end

defmodule Example.CLive do
  use Phoenix.LiveView, layout: {Example.Layouts, :app}

  def render(assigns) do
    ~H"""
    <h1>C</h1>
    <button phx-click="go">To A</button>
    """
  end

  def handle_event("go", _unsigned_params, socket) do
    {:noreply, socket |> put_flash(:info, "Flash from C") |> push_navigate(to: "/a")}
  end
end

defmodule Example.Router do
  use Phoenix.Router, helpers: false

  import Plug.Conn
  import Phoenix.Controller
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
    plug(:fetch_session)
    plug(:fetch_live_flash)
  end

  scope "/", Example do
    pipe_through(:browser)

    live_session :a_b_session do
      live("/a", ALive, :index)
      live("/b", BLive, :index)
    end

    live_session :c_session do
      live("/c", CLive, :index)
    end
  end
end

defmodule Example.Endpoint do
  use Phoenix.Endpoint, otp_app: :sample

  @session_options [
    store: :cookie,
    key: "_example_key",
    signing_salt: "De3iCH6m",
    same_site: "Lax"
  ]

  socket("/live", Phoenix.LiveView.Socket)

  plug(Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix")
  plug(Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view")
  plug(Plug.Session, @session_options)
  plug(Example.Router)
end

{:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions