defmodule PhoenixKit.Modules.Shop.Web.CartPage do @moduledoc """ Public cart page LiveView for E-Commerce module. """ use PhoenixKitWeb, :live_view alias PhoenixKit.Modules.Billing.Currency alias PhoenixKit.Modules.Shop alias PhoenixKit.Modules.Shop.ShippingMethod alias PhoenixKit.Utils.Routes @impl true def mount(_params, session, socket) do # Get session_id from session (for guest users) session_id = session["shop_session_id"] || generate_session_id() # Get current user if logged in user = get_current_user(socket) user_id = if user, do: user.id, else: nil # Get or create cart {:ok, cart} = Shop.get_or_create_cart(user_id: user_id, session_id: session_id) # Get available shipping methods shipping_methods = Shop.get_available_shipping_methods(cart) # Auto-select cheapest shipping method if none selected {:ok, cart} = Shop.auto_select_shipping_method(cart, shipping_methods) # Get default currency from Billing currency = Shop.get_default_currency() # Check if user is authenticated authenticated = not is_nil(socket.assigns[:phoenix_kit_current_user]) socket = socket |> assign(:page_title, "Shopping Cart") |> assign(:cart, cart) |> assign(:session_id, session_id) |> assign(:shipping_methods, shipping_methods) |> assign(:currency, currency) |> assign(:authenticated, authenticated) {:ok, socket} end @impl true def handle_event("update_quantity", %{"item_id" => item_id, "quantity" => quantity}, socket) do item_id = String.to_integer(item_id) quantity = max(1, String.to_integer(quantity)) update_item_quantity(socket, item_id, quantity) end @impl true def handle_event("remove_item", %{"item_id" => item_id}, socket) do item_id = String.to_integer(item_id) item = Enum.find(socket.assigns.cart.items, &(&1.id == item_id)) if item do case Shop.remove_from_cart(item) do {:ok, updated_cart} -> shipping_methods = Shop.get_available_shipping_methods(updated_cart) {:noreply, socket |> assign(:cart, updated_cart) |> assign(:shipping_methods, shipping_methods) |> put_flash(:info, "Item removed from cart")} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to remove item")} end else {:noreply, socket} end end @impl true def handle_event("select_shipping", %{"method_id" => method_id}, socket) do method_id = String.to_integer(method_id) method = Enum.find(socket.assigns.shipping_methods, &(&1.id == method_id)) cart = socket.assigns.cart if method do # Country will be set at checkout based on billing info case Shop.set_cart_shipping(cart, method, nil) do {:ok, updated_cart} -> {:noreply, assign(socket, :cart, updated_cart)} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to set shipping method")} end else {:noreply, socket} end end @impl true def handle_event("proceed_to_checkout", _params, socket) do cart = socket.assigns.cart cond do cart.items == [] -> {:noreply, put_flash(socket, :error, "Your cart is empty")} is_nil(cart.shipping_method_id) -> {:noreply, put_flash(socket, :error, "Please select a shipping method")} true -> {:noreply, push_navigate(socket, to: Routes.path("/checkout"))} end end defp update_item_quantity(socket, item_id, quantity) do item = Enum.find(socket.assigns.cart.items, &(&1.id == item_id)) if item do case Shop.update_cart_item(item, quantity) do {:ok, updated_cart} -> shipping_methods = Shop.get_available_shipping_methods(updated_cart) {:noreply, socket |> assign(:cart, updated_cart) |> assign(:shipping_methods, shipping_methods)} {:error, _} -> {:noreply, put_flash(socket, :error, "Failed to update quantity")} end else {:noreply, socket} end end @impl true def render(assigns) do ~H""" <.shop_layout {assigns}>

Shopping Cart

<.link navigate={Routes.path("/shop")} class="btn btn-outline btn-sm gap-2"> <.icon name="hero-arrow-left" class="w-4 h-4" /> Continue Shopping
<%!-- Cart Items --%>
<%= if @cart.items == [] do %>
<.icon name="hero-shopping-cart" class="w-16 h-16 mx-auto mb-4 opacity-30" />

Your cart is empty

Add some products to get started

<.link navigate={Routes.path("/shop")} class="btn btn-primary"> Browse Products
<% else %>
<%= for item <- @cart.items do %> <% end %>
Product Quantity Price
<%= if item.product_image do %>
{item.product_title}
<% else %>
<.icon name="hero-cube" class="w-8 h-8 opacity-30" />
<% end %>
{item.product_title}
<%= if item.product_sku do %>
SKU: {item.product_sku}
<% end %> <%= if item.compare_at_price && Decimal.compare(item.compare_at_price, item.unit_price) == :gt do %>
{format_price(item.compare_at_price, @currency)} On sale!
<% end %>
{format_price(item.line_total, @currency)}
{format_price(item.unit_price, @currency)} each
<% end %> <%!-- Shipping Section --%> <%= if @cart.items != [] do %>

Shipping Method

<%= if @shipping_methods == [] do %>
<.icon name="hero-exclamation-triangle" class="w-5 h-5" /> No shipping methods available for your selection
<% else %>
<%= for method <- @shipping_methods do %> <% end %>
<% end %>
<% end %>
<%!-- Order Summary --%>

Order Summary

Subtotal ({@cart.items_count || 0} items) {format_price(@cart.subtotal, @currency)}
Shipping <%= if is_nil(@cart.shipping_method_id) do %> Select method <% else %> <%= if Decimal.compare(@cart.shipping_amount || Decimal.new("0"), Decimal.new("0")) == :eq do %> FREE <% else %> {format_price(@cart.shipping_amount, @currency)} <% end %> <% end %>
<%= if @cart.discount_amount && Decimal.compare(@cart.discount_amount, Decimal.new("0")) == :gt do %>
Discount -{format_price(@cart.discount_amount, @currency)}
<% end %> <%= if @cart.tax_amount && Decimal.compare(@cart.tax_amount, Decimal.new("0")) == :gt do %>
Tax {format_price(@cart.tax_amount, @currency)}
<% end %>
Total {format_price(@cart.total, @currency)}
<%= if @cart.items != [] do %>

Secure checkout powered by PhoenixKit

<% end %>
""" end # Layout wrapper - uses dashboard for authenticated, app_layout for guests slot :inner_block, required: true defp shop_layout(assigns) do ~H""" <%= if @authenticated do %> {render_slot(@inner_block)} <% else %> {render_slot(@inner_block)} <% end %> """ end # Private helpers defp generate_session_id do :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) end defp format_price(nil, _currency), do: "-" defp format_price(amount, %Currency{} = currency) do Currency.format_amount(amount, currency) end defp format_price(amount, nil) do "$#{Decimal.round(amount, 2)}" end defp get_current_user(socket) do case socket.assigns[:phoenix_kit_current_scope] do %{user: %{id: _} = user} -> user _ -> nil end end end