@repobit/dex-store
v1.6.16
Published
Client for pricing API
Readme
@repobit/dex-store
Typed, framework‑agnostic pricing/store core with pluggable providers, rich product/option models, and convenient helpers for formatting, overrides, and experimentation.
- Fetch products/options via built‑in providers (init, vlaicu) or your own
- Strongly typed
ProductandProductOptionAPIs - Locale/currency formatting with overrideable formatter
- Option overrides and campaign resolver hooks
- Trial link mapping and buy‑link transformers
Install
npm i @repobit/dex-storeAt a glance
import { Store } from '@repobit/dex-store';
const store = new Store({
locale : 'en-us',
provider: { name: 'vlaicu' },
// Optional async campaign resolver per product id
campaign: async ({ id, campaign }) => campaign,
// Optional per-variation overrides (keyed as `${devices}-${subscription}`)
overrides: {
'com.bitdefender.tsmd.v2': {
campaign: 'WINTER2025',
options : {
'5-12': { discountedPrice: 49.99 },
// delete a variation by setting null
'1-1' : null
}
}
},
// Optional trial-link mapping: productAlias → campaign → variation → trial-days → URL
trialLinks: {
'com.bitdefender.tsmd.v2': {
default: {
'5-12': { '30 days': 'https://example.test/trial?devices=5&months=12' }
}
}
},
// Optional link transformers for A/B, UTM, etc.
transformers: {
buyLink: async ({ buyLink }) => new URL(buyLink).toString(),
trialLink: async ({ name, trialLink, buyLink }) => {
const url = new URL(trialLink);
url.searchParams.set('trial', name);
url.searchParams.set('buyLink', buyLink);
return url.toString();
}
},
// Optional number -> string formatter (defaults to Intl currency)
formatter: ({ price, currency, locale }) => {
if (!currency) return price; // return number when currency missing
return new Intl.NumberFormat(locale ?? 'en-us', { style: 'currency', currency }).format(price);
}
});
const product = await store.getProduct({ id: 'com.bitdefender.tsmd.v2' });
const option = await product?.getOption({ devices: 5, subscription: 12 });
console.log({
price : option?.getPrice(), // "$109.99"
discounted : option?.getDiscountedPrice(), // "$59.99"
discount : option?.getDiscount(), // "$50"
buyLink : option?.getBuyLink(),
trialLink : option?.getTrialLink('30 days'),
devices : option?.getDevices(), // 5
subscription : option?.getSubscription() // 12
});Store
new Store(config: {
provider : { name: 'init' | 'vlaicu' } | new (...args) => Provider,
locale : `${string}-${string}`,
campaign? : ({ id, campaign }: { id: string; campaign?: string }) => Promise<string | undefined>,
overrides? : {
[productId: string]: {
campaign?: string,
options : { [variation: `${string}-${string}`]: Partial<UnboundProductOptionData> | null | undefined }
}
},
trialLinks?: {
[productAlias: string]: {
default?: { [variation: `${string}-${string}`]: { [trialDays: string]: string } },
[campaign: string]: { [variation: `${string}-${string}`]: { [trialDays: string]: string } } | undefined
}
},
transformers?: { buyLink: (param: { buyLink: string, product: UnboundProductData, option: UnboundProductOptionData }) => Promise<string> },
formatter? : (p: { price: number; currency?: string; locale?: Intl.UnicodeBCP47LocaleIdentifier }) => string | number
})getProduct(selector)returns aProduct | undefined(or an array if you pass an array of selectors).- Built‑in providers:
init: legacy Init Selectorvlaicu: platform API v1
- The store caches by adapted product id + campaign. If you ask for the same product id with different raw ids that adapt to the same platform id, options are aggregated.
Price formatting
- The store exposes a
formatterhook. Defaults to Intl currency viaformatPrice. - All string price/discount getters (
getPrice(),getDiscountedPrice(),getDiscount()) callstore.formatPriceunder the hood when returning strings. - If you pass
{ currency: false }/{ symbol: false }, the numeric variants are returned.
Product
Represents a product with many options (variations). Computes min/max aggregates for price and discount across all options (including monthly breakdowns).
Key methods:
product.getId(): string
product.getName(): string
product.getCampaign(): string | undefined
product.getCurrency(): string
product.getOption(): Promise<ProductOption[]> // all options
product.getOption({ devices, subscription }): Promise<ProductOption | undefined>
product.getPrice({ monthly?: boolean, currency?: boolean }): { min: string|number, max: string|number }
product.getDiscountedPrice({ monthly?: boolean, currency?: boolean }): { min: string|number, max: string|number }
product.getDiscount({ percentage?: boolean, symbol?: boolean }): { min: string|number, max: string|number }
product.getDevices(): { min: number, max: number, values: number[] }
product.getSubscriptions(): { min: number, max: number, values: number[] }ProductOption
A single product variation.
option.getVariation(): `${devices}-${subscription}`
option.getDevices(): number
option.getSubscription(): number
// Price
option.getPrice({ monthly?: boolean, currency?: boolean }): string | number
option.getDiscountedPrice({ monthly?: boolean, currency?: boolean }): string | number
option.getDiscount({ percentage?: boolean, symbol?: boolean, monthly?: boolean }): string | number
// Links
option.getBuyLink(): string
option.getTrialLink(trialDays: string): string | undefined
// Navigate
option.getOption({ devices?: number; subscription?: number }): Promise<ProductOption | undefined>
option.nextOption({ devices?: 'prev' | 'next' | number; subscription?: 'prev' | 'next' | number }): Promise<ProductOption | undefined>
// Bundling
option.toogleBundle({ option: ProductBundleOption; devicesFixed?: boolean; subscriptionFixed?: boolean }): Promise<ProductOption | undefined>Notes:
- String getters use the store’s formatter; numeric variants are returned when asked via
{ currency: false }or{ symbol: false }. nextOptionaccepts deltas per dimension.'next'/'prev'move by one index. Numeric deltas are relative.
Overrides and trial links
overridesmutate/patch individual options before instantiation. Set a variation tonullto drop it.trialLinksmap trial-period keys (e.g.'30 days','45 days') to URLs for each product › campaign › option variation. Multiple durations per variation are supported. Retrieve withoption.getTrialLink('30 days').transformers.option.buyLinklets you rewrite buy links uniformly (e.g., add UTM, swap host).
Custom providers
Implement your own by extending the abstract Provider:
import { Provider } from '@repobit/dex-store/src/providers/provider.base';
import type { UnboundProductData } from '@repobit/dex-store/src/products/product.base';
class MyProvider extends Provider {
async fetch({ id, campaign }): Promise<UnboundProductData | undefined> {
// call your API, adapt { devices, subscription } using this.adaptTo(...)
return {
id,
name: 'My Product',
currency: 'USD',
campaign,
campaignType: '',
platformId: 'P123',
options: new Map([
['5-12', {
devices: 5,
subscription: 12,
price: 109.99,
discountedPrice: 59.99,
buyLink: 'https://example/order',
// trialLinks are applied from the store config; no need to set them in option data
bundle: []
}]
])
};
}
}
const store = new Store({ locale: 'en-us', provider: MyProvider });Formatting helper
formatPrice({ price, currency, locale }) is exported for convenience. It uses Intl.NumberFormat and returns a string when currency is provided, otherwise the raw number.
Caching and aggregation
- The store caches results per adapted product id + campaign. The adapter can normalize/alias ids before the fetch.
- If you request the same logical product using multiple raw ids that adapt to the same platform id, the store aggregates options:
- First fetch stores a base Product instance in the cache.
- Later fetches for the same cache key merge their options into the base via
SET_OPTION, so you see a superset of options.
- This is helpful when product variations are delivered under multiple source ids that map to a single platform product.
Built‑in providers
init(Init Selector)- Posts a small JSON payload to Bitdefender’s selector endpoint, returns a nested map of variations.
- Computes buy links, adapts
{ devices, subscription }, and extracts discounted price when available. - Respects the
campaignresolver and an internal ignore list (to disable promotions).
vlaicu(Platform API v1)- Fetches a flat list of options with
{ slots, months, price, discountedPrice, buyLink }. - Adapts
{ devices, subscription }using the store adapter. - Uses store locale for currency formatting.
- Fetches a flat list of options with
Both providers:
- Respect
overrides,trialLinksandtransformers.buyLink. - Use the store’s adapter for id/variation normalization.
Troubleshooting
Price shows as a number instead of a formatted string
- Ensure the product provides a currency, or pass a
formatterin Store config.
- Ensure the product provides a currency, or pass a
getOption/nextOptionreturnsundefined- The requested
{ devices, subscription }is outside the product’s min/max or not present in the values list.
- The requested
Overrides had no effect
- Check the
overrideskey matches the product id returned by the provider (after adaptation). - Ensure variation keys are
${devices}-${subscription}strings.
- Check the
Trial link isn’t set
- Confirm
trialLinksis keyed by product alias → campaign → variation → trial days, e.g.{ productAlias: { default: { '5-12': { '30 days': url } } } }. - Verify
option.getTrialLink('30 days')is called with the exact trial-days string used as the key.
- Confirm
Different ids return a single product
- That’s expected when ids adapt to the same platform id; options are aggregated into one Product instance.
License
ISC
