PhoenixKitEcommerce.Web.Helpers (PhoenixKitEcommerce v0.5.11)

Copy Markdown View Source

Shared helper functions for Shop public LiveViews.

Centralizes utility functions that were duplicated across shop_catalog, catalog_category, catalog_product, cart_page, checkout_page, and checkout_complete.

Summary

Functions

Where the storefront's admin edit link should point for kind and uuid, and where the editor should send the visitor back to.

Find the best enabled language that has a slug for this entity.

Build a localized URL path, adding language prefix for non-default languages. Delegates to Routes.path which handles default vs non-default consistently.

Get the first image URL for a product.

Format a price value with currency. Returns "-" for nil price.

Clears the schedule_fx_reload/2 flag; call first in handle_info(:fx_reload, _).

Extract current user from socket assigns scope.

Determine language from URL params.

Get signed URL for a Storage image file.

Whether the storefront drops an all-zero fractional part ("40" rather than "40.00").

Convert a key string to human-readable format.

Assigns :admin_edit_url/:admin_edit_label on socket for an admin visitor, via core's PhoenixKitWeb.AdminEditHelper.assign_admin_edit/3.

The billing identity an order was placed with.

Parse an integer from a LiveView event payload, falling back to default.

Parse page param with validation. Returns 1 for invalid/missing values.

Format address for a billing profile struct or an order's snapshot map.

Format display name for a billing profile.

Contact email from a billing profile STRUCT or an order's snapshot MAP.

Point this module's Gettext backend at language, falling back to the base language when the catalogue has no dialect.

Sets the content locale from a socket, falling back to the shop's CONFIGURED default rather than a hardcoded "en".

Re-marks @currency as changed so every price expression that reads it re-evaluates on the next render — the ONE way a mounted storefront picks up a currency-table change (§4.2.1 п.5).

Coalesces the product RELOAD a {:currencies_changed, _} broadcast asks for into one :fx_reload message per burst.

Whether the storefront's category navigation is switched on (shop_sidebar_show_categories, default true). Read by every public page that renders a category list, so the catalog aside, the catalog grid and the product page's category panel cannot disagree about the default.

Whether a product's tags may be shown on a page rendered in language.

Functions

admin_edit_path(kind, uuid, return_to \\ nil)

@spec admin_edit_path(:item | :category, String.t(), String.t() | nil) :: String.t()

Where the storefront's admin edit link should point for kind and uuid, and where the editor should send the visitor back to.

The shop's products and categories live in phoenix_kit_catalogue once the catalogue product source is on, so the link has to open the catalogue editor rather than the legacy shop form, which no longer backs the page being viewed. return_to is where the editor should send the visitor back to — both catalogue forms validate it (safe_return_to/1) and use it for their exit, so "edit, save, back to the page I was on" works without reaching for the browser's back button. It is the page's canonical URL rather than the exact one the visitor typed, and query state (a filter, a page number) is not carried back. Two reasons, and the second is the one that matters: these links are built in mount/3, while core assigns :url_path from its handle_params hook, which runs later — but :url_path is a path, parsed out of the URL with the query discarded, so building the link later would not carry the query either.

Falls back to the legacy shop path when the catalogue source is off or the catalogue module isn't loaded at all (it is an optional dependency).

best_redirect_language(slug_map)

Find the best enabled language that has a slug for this entity.

Prefers the default language, then checks other enabled languages. Returns nil if no valid language found.

build_lang_url(path, lang)

Build a localized URL path, adding language prefix for non-default languages. Delegates to Routes.path which handles default vs non-default consistently.

first_image(arg1)

Get the first image URL for a product.

Handles Storage-based images (new format with featured_image_uuid or image_uuids) and legacy URL-based images (Shopify imports). Returns nil if no image is available.

format_price(price, currency)

Format a price value with currency. Returns "-" for nil price.

Accepts the amount as a Decimal, number, or numeric string (order line items persist their amounts as strings). The currency may be a Currency struct, a bare code string (Shop.currency_for_code/1 falls back to the code when the record's currency no longer resolves — showing "12.50 XYZ" is honest, borrowing today's default symbol is not), or nil (no default currency configured at all — legacy $).

fx_reload_done(socket)

Clears the schedule_fx_reload/2 flag; call first in handle_info(:fx_reload, _).

get_current_user(socket)

Extract current user from socket assigns scope.

get_language_from_params_or_default(arg1)

Determine language from URL params.

Uses locale param if present, otherwise falls back to Translations.default_language/0. Used by catalog and category pages (non-product pages).

get_storage_image_url(file_uuid, variant)

Get signed URL for a Storage image file.

Returns nil if file or variant not found (unlike product detail page which returns a placeholder). Falls back to original variant if requested variant is not available.

hide_zero_decimals?()

Whether the storefront drops an all-zero fractional part ("40" rather than "40.00").

Off by default, because dropping the decimals is wrong for most shops. It exists for shops whose prices are round by nature — services quoted in whole units, where "40.00 EUR" reads as unnecessarily precise and, as one operator put it, faintly alarming.

Storefront only. Invoices, receipts and credit notes keep two decimals: they are accounting documents, and this setting must never reach them.

humanize_key(key)

Convert a key string to human-readable format.

Example: "material_type" -> "Material Type"

maybe_assign_admin_edit(socket, path, label, opts \\ [])

Assigns :admin_edit_url/:admin_edit_label on socket for an admin visitor, via core's PhoenixKitWeb.AdminEditHelper.assign_admin_edit/3.

permission is what the LINKED PAGE actually requires, and it defaults to "shop.manage_catalog" because most of these links open a catalog editor. Core's own helper gates on "can this visitor reach the admin area at all", which is broader: an admin with, say, only order-desk permissions used to be shown an Edit button that landed them on a form they could not submit. But the gate must not run ahead of the target either — the shop index's "Manage Shop" link opens the dashboard at /admin/shop, which asks for base "shop" — so that call site passes its own weaker permission rather than inheriting the catalog one.

Guarded with Code.ensure_loaded?/1 + function_exported?/3 rather than calling the helper directly: ecommerce pins phoenix_kit with a ~> requirement, not an exact version, so a host running an older core that predates the helper must not crash storefront pages. Returns socket unchanged when the helper isn't available or the visitor isn't an admin.

order_billing_identity(arg1)

The billing identity an order was placed with.

Prefers the order's immutable billing_snapshot over the live billing profile: the profile is editable, so reading it made a historical order claim an address it was never billed to (and deleting the profile made the true one reappear). The live profile is a fallback only for orders placed before snapshots existed.

parse_int(value, default \\ 0)

Parse an integer from a LiveView event payload, falling back to default.

String.to_integer/1 raises on anything non-numeric, and a raise inside handle_event/3 takes the whole LiveView down — so any hand-crafted or merely stale phx-value-* produced a crashed socket rather than an ignored event. That is reachable unauthenticated on the storefront (quantity fields) and by any admin elsewhere.

Returns default for nil, blank, partially-numeric ("3abc") and non-binary input. Callers that need a floor should still apply one — this only guarantees you get an integer back.

parse_page(page)

Parse page param with validation. Returns 1 for invalid/missing values.

profile_address(profile)

Format address for a billing profile struct or an order's snapshot map.

profile_display_name(profile)

Format display name for a billing profile.

profile_email(profile)

Contact email from a billing profile STRUCT or an order's snapshot MAP.

The snapshot is a plain map, so profile.email raises on it — the crash a rendered confirmation page hit after order pages started preferring the snapshot. Same shape problem as profile_display_name/1.

put_content_locale(language)

Point this module's Gettext backend at language, falling back to the base language when the catalogue has no dialect.

Without this the storefront renders English in every locale, however complete the catalogues are. The content language here is a DIALECT (resolve_dialect/1 returns "ru-RU", "et-EE", "en-US"), and that is also what core puts into the process locale — but this module ships priv/gettext/{en,ru,et}, plain codes with no region. Gettext does not fall back from "ru-RU" to "ru" on its own, so every lookup missed and returned its msgid, which is the English source string.

Core's own catalogue has the same plain-code shape, so this is not specific to the shop; it is why a fully translated module can still render entirely in English. Verified on a dev box: put_locale("ru") translates, put_locale("ru-RU") does not.

Called from mount/3, which runs once per process for both the dead render and the connected mount, so the whole lifecycle of that LiveView is covered.

put_content_locale_from(socket)

Sets the content locale from a socket, falling back to the shop's CONFIGURED default rather than a hardcoded "en".

:current_locale is supplied by core's live_session on_mount; a host that mounts these LiveViews outside it gets nil, and a hardcoded English fallback would force English on a shop whose default language is Russian.

refresh_display_currency(socket)

@spec refresh_display_currency(Phoenix.LiveView.Socket.t()) ::
  Phoenix.LiveView.Socket.t()

Re-marks @currency as changed so every price expression that reads it re-evaluates on the next render — the ONE way a mounted storefront picks up a currency-table change (§4.2.1 п.5).

The rate is deliberately not in assigns (§12.4), so nothing in the socket knows it moved; Phoenix.Component.assign/3 skips an equal value and HEEx re-evaluates an expression only when one of ITS assigns changed. Passing through a sentinel value marks the key changed while the code itself stays what the request resolved. Base numbers (@products, @calculated_price) are unchanged and are not re-read.

schedule_fx_reload(socket, delay_ms \\ 250)

Coalesces the product RELOAD a {:currencies_changed, _} broadcast asks for into one :fx_reload message per burst.

Billing broadcasts once per currency per rate refresh, and a base change touches every row — so a storefront tab that re-queried its products on every message ran N full catalog queries for one event, on every open tab at once. The cheap part (refresh_display_currency/1, re-marking @currency so loaded prices re-present) stays immediate; the re-fetch is deferred by delay_ms and scheduled at most once while :fx_reload_pending is set. The page's handle_info(:fx_reload, _) clause clears the flag (fx_reload_done/1) and does the reload.

tags_visible?(language)

@spec tags_visible?(String.t() | nil) :: boolean()

Whether a product's tags may be shown on a page rendered in language.

Tags arrive from Shopify as one untranslated list on data["ecommerce"]["tags"] — there is no per-language variant of them. Rendering that list on a translated page puts the only untranslated text on the card, so tags stay on the default-language storefront and are hidden elsewhere until translated tags exist.