All checks were successful
deploy / deploy (push) Successful in 1m26s
Disable checkout when Stripe isn't connected (cart drawer, cart page, and early guard in checkout controller to prevent orphaned orders). Show amber warning on order detail when email isn't configured. Fix pre-existing missing vertical spacing between page blocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
195 lines
5.8 KiB
Elixir
195 lines
5.8 KiB
Elixir
defmodule BerrypodWeb.CartHook do
|
|
@moduledoc """
|
|
LiveView on_mount hook for cart state and shared event handling.
|
|
|
|
Mounted in the public_shop live_session to give all shop LiveViews
|
|
cart state, PubSub sync, and shared event handlers via attach_hook.
|
|
|
|
Handles these events so individual LiveViews don't have to:
|
|
- `open_cart_drawer` / `close_cart_drawer` - toggle drawer visibility
|
|
- `remove_item` - remove item from cart
|
|
- `increment` / `decrement` - change item quantity
|
|
- `change_country` - update shipping country
|
|
- `{:cart_updated, cart}` info - cross-tab cart sync via PubSub
|
|
|
|
LiveViews with custom cart logic (e.g. add_to_cart) can call
|
|
`update_cart_assigns/2` and `broadcast_and_update/2` directly.
|
|
"""
|
|
|
|
import Phoenix.Component, only: [assign: 3]
|
|
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, push_event: 3]
|
|
|
|
alias Berrypod.{Cart, Settings, Shipping}
|
|
|
|
def on_mount(:mount_cart, _params, session, socket) do
|
|
cart_items = Cart.get_from_session(session)
|
|
country_code = session["country_code"] || "GB"
|
|
available_countries = Shipping.list_available_countries_with_names()
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:country_code, country_code)
|
|
|> assign(:available_countries, available_countries)
|
|
|> update_cart_assigns(cart_items)
|
|
|> assign(:cart_drawer_open, false)
|
|
|> assign(:cart_status, nil)
|
|
|> assign(:stripe_connected, Settings.has_secret?("stripe_api_key"))
|
|
|> attach_hook(:cart_events, :handle_event, &handle_cart_event/3)
|
|
|> attach_hook(:cart_info, :handle_info, &handle_cart_info/2)
|
|
|
|
socket =
|
|
if connected?(socket) do
|
|
csrf_token = Map.get(session, "_csrf_token", "default")
|
|
topic = "cart:#{csrf_token}"
|
|
Phoenix.PubSub.subscribe(Berrypod.PubSub, topic)
|
|
assign(socket, :cart_topic, topic)
|
|
else
|
|
assign(socket, :cart_topic, nil)
|
|
end
|
|
|
|
{:cont, socket}
|
|
end
|
|
|
|
# Shared event handlers
|
|
|
|
defp handle_cart_event("open_cart_drawer", _params, socket) do
|
|
{:halt, assign(socket, :cart_drawer_open, true)}
|
|
end
|
|
|
|
defp handle_cart_event("close_cart_drawer", _params, socket) do
|
|
{:halt, assign(socket, :cart_drawer_open, false)}
|
|
end
|
|
|
|
defp handle_cart_event("change_country", %{"country" => code}, socket) do
|
|
socket =
|
|
socket
|
|
|> assign(:country_code, code)
|
|
|> update_cart_assigns(socket.assigns.raw_cart)
|
|
|> push_event("persist_country", %{code: code})
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_event("remove_item", %{"id" => variant_id}, socket) do
|
|
cart = Cart.remove_item(socket.assigns.raw_cart, variant_id)
|
|
|
|
socket =
|
|
socket
|
|
|> broadcast_and_update(cart)
|
|
|> assign(:cart_status, "Item removed from cart")
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_event("increment", %{"id" => variant_id}, socket) do
|
|
cart = Cart.add_item(socket.assigns.raw_cart, variant_id, 1)
|
|
new_qty = Cart.get_quantity(cart, variant_id)
|
|
|
|
socket =
|
|
socket
|
|
|> broadcast_and_update(cart)
|
|
|> assign(:cart_status, "Quantity updated to #{new_qty}")
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_event("decrement", %{"id" => variant_id}, socket) do
|
|
current = Cart.get_quantity(socket.assigns.raw_cart, variant_id)
|
|
cart = Cart.update_quantity(socket.assigns.raw_cart, variant_id, current - 1)
|
|
new_qty = Cart.get_quantity(cart, variant_id)
|
|
|
|
socket =
|
|
socket
|
|
|> broadcast_and_update(cart)
|
|
|> assign(:cart_status, "Quantity updated to #{new_qty}")
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_event(
|
|
"update_quantity_form",
|
|
%{"variant_id" => id, "quantity" => qty_str},
|
|
socket
|
|
) do
|
|
quantity = String.to_integer(qty_str)
|
|
cart = Cart.update_quantity(socket.assigns.raw_cart, id, quantity)
|
|
new_qty = Cart.get_quantity(cart, id)
|
|
|
|
socket =
|
|
socket
|
|
|> broadcast_and_update(cart)
|
|
|> assign(:cart_status, "Quantity updated to #{new_qty}")
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_event("remove_item_form", %{"variant_id" => id}, socket) do
|
|
cart = Cart.remove_item(socket.assigns.raw_cart, id)
|
|
|
|
socket =
|
|
socket
|
|
|> broadcast_and_update(cart)
|
|
|> assign(:cart_status, "Item removed from cart")
|
|
|
|
{:halt, socket}
|
|
end
|
|
|
|
defp handle_cart_event(_event, _params, socket), do: {:cont, socket}
|
|
|
|
# Shared info handlers
|
|
|
|
defp handle_cart_info({:cart_updated, cart}, socket) do
|
|
{:halt, update_cart_assigns(socket, cart)}
|
|
end
|
|
|
|
defp handle_cart_info(_msg, socket), do: {:cont, socket}
|
|
|
|
# Public helpers for LiveViews with custom cart logic
|
|
|
|
@doc """
|
|
Updates all cart-related assigns from raw cart data.
|
|
"""
|
|
def update_cart_assigns(socket, cart) do
|
|
%{items: items, count: count, subtotal: subtotal} = Cart.build_state(cart)
|
|
country_code = socket.assigns[:country_code] || "GB"
|
|
subtotal_pence = Cart.calculate_subtotal(items)
|
|
|
|
shipping_estimate =
|
|
case Shipping.calculate_for_cart(items, country_code) do
|
|
{:ok, cost} when cost > 0 -> cost
|
|
_ -> nil
|
|
end
|
|
|
|
cart_total =
|
|
Cart.format_price(subtotal_pence + (shipping_estimate || 0))
|
|
|
|
socket
|
|
|> assign(:raw_cart, cart)
|
|
|> assign(:cart_items, items)
|
|
|> assign(:cart_count, count)
|
|
|> assign(:cart_subtotal, subtotal)
|
|
|> assign(:cart_total, cart_total)
|
|
|> assign(:shipping_estimate, shipping_estimate)
|
|
end
|
|
|
|
@doc """
|
|
Broadcasts cart update to other tabs and updates local assigns.
|
|
|
|
Uses broadcast_from to avoid notifying self (prevents double-update).
|
|
"""
|
|
def broadcast_and_update(socket, cart) do
|
|
if socket.assigns.cart_topic do
|
|
Phoenix.PubSub.broadcast_from(
|
|
Berrypod.PubSub,
|
|
self(),
|
|
socket.assigns.cart_topic,
|
|
{:cart_updated, cart}
|
|
)
|
|
end
|
|
|
|
socket
|
|
|> update_cart_assigns(cart)
|
|
|> push_event("persist_cart", %{items: Cart.serialize(cart)})
|
|
end
|
|
end
|