Description
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)