@repobit/dex-store
v1.3.10
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 mapping of trial links per variation
trialLinks: {
'5-12': 'https://example.test/trial?devices=5&months=12'
},
// Optional buy-link transformer for A/B, UTM, etc.
transformers: {
option: {
buyLink: async (href) => new URL(href).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(),
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?: { [variation: `${string}-${string}`]: string },
transformers?: { option?: { buyLink: (href: string) => 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(): string
// 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.trialLinksinject per‑variation trial links as plain strings. Providers can still supply a trial link; the mapping overwrites it when present.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',
trialLink: 'https://example/trial',
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.option.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
trialLinkshas an entry for the variation and not overwritten later by the provider.
- 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
