Skip to main content

Utilities

Pure helpers grouped by topic. Every export is a free function or constant — no classes, no module-scoped state — so each one is trivially tree-shakeable. Import what you need from the package root:

import { formatPrice, isContact, deriveUserMode } from '@propeller-commerce/propeller-v2-core-ui';

Formatting​

formatPrice(amount: number, opts?: { currency?, locale?, symbol? }): string
formatDate(date: string | Date, opts?: { locale?, format? }): string
formatSurcharge(surcharge: number, opts?: { currency?, locale? }): string
calcDiscountPercent(original: number, current: number): number

formatPrice is locale-aware via Intl.NumberFormat; the symbol override exists for shops that display a non-currency-code symbol (e.g. '€' instead of 'EUR'). Throws on NaN/non-finite values — wrap in the Result contract at the boundary if upstream data is untrusted.

Attribute extraction​

attributeNameMatches(attr, name): boolean
getAttributeDisplayName(attr, language?): string
extractAttributeValues(product, attrName): string[]
collectAttributeValues(products, attrName): Set<string>
filterProductsBySelections(products, selections): Product[]

The Propeller GraphQL schema returns attributes as a flat array of { name, value, language? } records. These helpers handle the denormalisation: case-insensitive name matching, multi-language fallback via languageResolver, and selection-based filtering for facet UIs.

Inventory​

getStockStatus(quantity: number): { label: string, className: string }

Maps a stock quantity to a tri-state UI badge ("In stock" / "Low stock" / "Out of stock"). The className is a stable token; the framework UI packages map it to their own design-token palette.

Label fallback​

getLabel(labels: Record<string, string> | undefined, key: string, fallback: string): string

Every component in the UI packages accepts an optional labels prop. This helper is the canonical implementation — look up key, fall back to the hardcoded English string. Used in ~50 components.

Language resolution​

resolveLanguageEntry<T>(entries: LocalizedEntry<T>[], language: string): T | undefined
getLanguageString(entries, language, key): string | undefined
getLanguageUri(entries, language): string | undefined

Walks a LocalizedEntry[] array ({ language, value }) and resolves the right value with a sane fallback chain: requested locale → en → first entry. Used by attribute extraction, product helpers, and the CMS renderer.

Product helpers​

getProductImageUrl(product, opts?): string | null
getClusterImageUrl(cluster, opts?): string | null
getProductSku(product): string | null
getClusterSku(cluster): string | null
getLocalizedValue<T>(entries, language): T | undefined

Encapsulate the "default image / SKU" walks across the SDK's Product / Cluster shape (which can have many images and multiple SKUs depending on configuration).

Truncation​

stripHtml(html: string): string
shouldTruncate(text: string, max: number): boolean
truncateAt(text: string, max: number, ellipsis?: string): string

stripHtml is dependency-free — it removes tags but does not sanitise. Use it for length calculation and SEO-meta-tag derivation, not for rendering untrusted HTML.

User identity​

isContact(user: AnyUser): boolean // user is a B2B Contact
isCustomer(user: AnyUser): boolean // user is a B2C Customer
getUserId(user): string | null
getCompany(user): Company | null
getCompanyId(user): string | null
getAddresses(user): Address[]
getDefaultInvoiceAddress(user): Address | null
getDefaultDeliveryAddress(user): Address | null

The Propeller backend returns Contact or Customer as different SDK classes; consumers want a flat predicate. isContact uses field presence ('contactId' in user) so it works on plain-object-serialised users too (important for SSR hydration).

deriveUserMode​

deriveUserMode(user: AnyUser | null, shopMode: ShopMode): UserMode
// ShopMode = 'b2b' | 'b2c' | 'hybrid'
// UserMode = 'anonymous' | 'b2b' | 'b2c'

Reduces (user, shopMode) to the single runtime mode every component branches on. Anonymous users always resolve to 'anonymous' regardless of shop mode; logged-in users resolve via isContact/isCustomer.

In a 'b2b' shop, a Customer who somehow logged in is still treated as 'b2b' — the route guard kicks them out before this is reached, but the function is total either way.

const mode = deriveUserMode(currentUser, 'hybrid');
if (mode === 'b2b') showCompanySwitcher();

Visibility​

isContentHidden(
portalMode: PortalMode,
user: AnyUser | null,
isAuthenticated?: boolean,
): boolean

Implements the portal-mode rules for when to hide entire route trees (prices, account, catalog) from anonymous visitors. Used by every catalog component and the page guards.

isAuthenticated closes a hydration window. Hosts paint from a thin cached hint before the full profile arrives, so user is null for an authenticated visitor for the first frames — without the flag, a signed-in shopper sees the log-in call to action and blanked prices until getViewer() resolves. Note the host's own isLoading is already false by then, so it cannot serve this purpose; the signal has to be "a session exists".

It is optional, and omitting it keeps the previous behaviour — server renders, which have no such window, pass two arguments as before.

// Host bridge: feed it alongside `user`.
<PropellerProvider value={{ user: state.user, isAuthenticated: state.isAuthenticated, … }}>

Video URL normalisation​

isEmbeddable(url: string): boolean
normalizeVideoUrl(url: string): string | null

Accepts watch-style YouTube/Vimeo URLs and returns the embed equivalent. Returns null for unrecognised hosts; callers should fall back to a plain <a href>.

JSON-LD builders​

buildProductJsonLd(product, ctx: JsonLdContext): object
buildClusterJsonLd(cluster, ctx: JsonLdContext): object
buildItemListJsonLd(items, ctx: JsonLdContext): object
safeJsonStringify(value): string

Generate schema.org JSON-LD for product, cluster, and item-list pages. safeJsonStringify escapes </script> runs so the result is safe to embed in an inline <script type="application/ld+json">.

Countries​

COUNTRIES: Country[] // ISO 3166-1 alpha-2 + display name
COUNTRIES_MAP: Record<string, Country>
getCountryName(code: string, locale?: string): string | null

Used by address forms in the UI packages.