Skip to main content

ProductPrice

Displays a product's price with automatic VAT toggle support, secondary price line, cluster option price aggregation, and semi-closed portal mode for catalog-only storefronts.

Usage

Basic — Single Product Price

import ProductPriceDisplay from '@/components/propeller/ProductPrice';

<ProductPriceDisplay
price={product.price}
includeTax={true}
/>

Renders the incl. VAT price as the leading (large) value and excl. VAT as the secondary (small) line below it.

With VAT Toggle Integration

const [includeTax, setIncludeTax] = useState(true);

useEffect(() => {
const stored = localStorage.getItem('price_include_tax');
if (stored !== null) setIncludeTax(stored === 'true');

const handler = (e: CustomEvent) => setIncludeTax(e.detail);
window.addEventListener('priceToggleChanged', handler as EventListener);
return () => window.removeEventListener('priceToggleChanged', handler as EventListener);
}, []);

<ProductPriceDisplay
price={product.price}
includeTax={includeTax}
/>

When a PriceToggle component fires the priceToggleChanged event, the leading and secondary prices swap automatically.

Custom Currency and Size

<ProductPriceDisplay
price={product.price}
includeTax={true}
currency="$"
priceSize="text-xl"
/>

Semi-Closed Portal (Catalog Mode)

<ProductPriceDisplay
price={product.price}
portalMode="semi-closed"
user={currentUser}
labels={{ loginToSeePrices: 'Sign in for pricing' }}
/>

When portalMode is "semi-closed" and no user is provided, the price is hidden and replaced with a login prompt.

Cluster Product with Options

<ProductPriceDisplay
price={cluster.price}
includeTax={true}
options={cluster.options}
selectedOptionProducts={Object.values(selectedOptions)}
/>

The component automatically sums the base price with prices from selected option products. For required options where the user has not yet made a selection, the default product's price is included.

Custom Labels

<ProductPriceDisplay
price={product.price}
includeTax={true}
labels={{
inclTax: 'inc. BTW',
exclTax: 'ex. BTW',
loginToSeePrices: 'Inloggen voor prijzen',
}}
/>

Configuration

Core

PropTypeDefaultDescription
priceProductPricerequiredThe price object from product.price. Contains net (incl. VAT) and gross (excl. VAT) values.
includeTaxbooleanfalseWhen true, the incl. VAT price (price.net) is the leading price. When false, the excl. VAT price (price.gross) leads.
currencystring'€'Currency symbol prepended to all displayed prices.

Cluster Options

PropTypeDefaultDescription
optionsClusterOption[]undefinedCluster option groups from cluster.options. Used to calculate the total price including option selections.
selectedOptionProductsProduct[]undefinedCurrently selected option products. Pass Object.values(selectedOptionProducts) from your selection state. The displayed price recalculates whenever this array changes.

Portal / Visibility

PropTypeDefaultDescription
portalModestring'open''open' shows prices to everyone. 'semi-closed' hides prices for anonymous visitors and shows a login prompt instead.
userContact | Customer | nullundefinedThe authenticated user. Required for semi-closed mode to determine whether to show or hide prices.

Labels and Styling

PropTypeDefaultDescription
labelsRecord<string, string>undefinedOverride UI strings. Available keys: inclTax (default 'incl. VAT'), exclTax (default 'excl. VAT'), loginToSeePrices (default 'Log in to see prices').
priceSizestring'text-3xl'Tailwind text-size class applied to the leading price.
classNamestringundefinedExtra CSS class applied to the root <div>.
taxZonestring'NL'Tax zone code (reserved for future use).

Labels

KeyDefaultDescription
inclTax'incl. VAT'Label shown next to the incl. VAT price
exclTax'excl. VAT'Label shown next to the excl. VAT price
loginToSeePrices'Log in to see prices'Message shown in semi-closed portal mode when no user is present

Behavior

Price Toggle (VAT Switch)

The component itself is stateless regarding VAT preference -- it renders based on the includeTax prop. The app-wide VAT toggle pattern works as follows:

  1. A PriceToggle component writes 'true' or 'false' to localStorage under the key price_include_tax.
  2. It dispatches a priceToggleChanged custom event with the new boolean as event.detail.
  3. Parent components listen for this event, update their local state, and pass it down as includeTax.

This decoupled pattern means ProductPrice works in any context -- with or without the toggle system.

Leading and Secondary Prices

The component always shows two price lines when both values are available:

  • Leading price: Large, bold. Controlled by includeTax. Shows the primary price with a tax label (e.g., "incl. VAT").
  • Secondary price: Small, muted. Shows the opposite tax variant below the leading price (e.g., "excl. VAT").

If either price.net or price.gross is null or undefined, that line is omitted.

Cluster Option Price Aggregation

When options and selectedOptionProducts are provided, the component adds option prices to the base price:

  1. Iterates all non-hidden cluster options.
  2. For each option, checks if a matching product exists in selectedOptionProducts.
  3. If found, adds that product's price (net or gross, matching the current includeTax setting).
  4. If no selection exists but the option is required (isRequired === 'Y'), the default product's price is added instead.
  5. The total is summed with the base price.net/price.gross for the final displayed value.

Semi-Closed Portal Mode

When portalMode="semi-closed":

  • No user provided: The entire price display is replaced with a login prompt message (customizable via labels.loginToSeePrices).
  • User provided: Prices display normally.

When portalMode="open" (the default), prices are always visible regardless of user state.

Price Formatting

All prices are formatted as {currency}{value} with exactly two decimal places. For example, €99.17. There are no thousands separators. The currency symbol defaults to but can be overridden via the currency prop.

SDK Services

Product Price Fields

The ProductPrice type from propeller-sdk-v2 contains two key numeric fields:

FieldMeaningExample
price.netPrice including VAT120.00
price.grossPrice excluding VAT99.17

This naming convention is counterintuitive but consistent across the Propeller SDK. The component abstracts this away via the includeTax prop:

  • includeTax={true} shows price.net as the leading price and price.gross as secondary.
  • includeTax={false} shows price.gross as the leading price and price.net as secondary.

Additional price fields that may be available on the ProductPrice object:

FieldPurpose
price.originalNetOriginal incl. VAT price before discount
price.originalGrossOriginal excl. VAT price before discount
price.discountDiscount percentage or amount
price.taxCodeApplied tax rate code

GraphQL Query Example

query GetProduct($productId: Int!) {
product(productId: $productId) {
productId
name {
value
}
price {
net
gross
originalNet
originalGross
discount
taxCode
}
}
}

Using the SDK:

import { ProductService } from 'propeller-sdk-v2';

const productService = new ProductService(graphqlClient);
const product = await productService.getProduct({ productId: 123 });

// product.price.net → incl. VAT
// product.price.gross → excl. VAT