@repobit/dex-store 0.3.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +238 -5
- package/dist/src/adaptors/adaptor.base.d.ts +1 -0
- package/dist/src/adaptors/adaptor.base.js +10 -8
- package/dist/src/adaptors/adaptor.base.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/product-options/option.base.d.ts +39 -7
- package/dist/src/product-options/option.base.js +146 -7
- package/dist/src/product-options/option.base.js.map +1 -1
- package/dist/src/products/product.base.d.ts +6 -4
- package/dist/src/products/product.base.js +11 -11
- package/dist/src/products/product.base.js.map +1 -1
- package/dist/src/products/product.init-selector.d.ts +2 -2
- package/dist/src/products/product.init-selector.js +2 -2
- package/dist/src/products/product.init-selector.js.map +1 -1
- package/dist/src/products/product.vlaicu.d.ts +2 -2
- package/dist/src/products/product.vlaicu.js +1 -1
- package/dist/src/products/product.vlaicu.js.map +1 -1
- package/dist/src/providers/provider.base.d.ts +9 -1
- package/dist/src/providers/provider.base.js +51 -14
- package/dist/src/providers/provider.base.js.map +1 -1
- package/dist/src/providers/provider.init-selector.d.ts +2 -3
- package/dist/src/providers/provider.init-selector.js +14 -7
- package/dist/src/providers/provider.init-selector.js.map +1 -1
- package/dist/src/providers/provider.vlaicu.d.ts +2 -3
- package/dist/src/providers/provider.vlaicu.js +15 -8
- package/dist/src/providers/provider.vlaicu.js.map +1 -1
- package/dist/src/store.d.ts +28 -3
- package/dist/src/store.js +10 -0
- package/dist/src/store.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.1.1](https://github.com/bitdefender/dex-core/compare/@repobit/dex-store@1.1.0...@repobit/dex-store@1.1.1) (2025-10-10)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **DEX-21811:** now adaptor know new id ([2fefad1](https://github.com/bitdefender/dex-core/commit/2fefad1f60c3a2d3caa1f997245599aee43e4284))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [1.1.0](https://github.com/bitdefender/dex-core/compare/@repobit/dex-store@0.3.0...@repobit/dex-store@1.1.0) (2025-10-08)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **DEX-21811:** add bundled options to the bundle item ([5e0ac67](https://github.com/bitdefender/dex-core/commit/5e0ac67709096278e1993188e524ab5ea2009248))
|
|
21
|
+
* **DEX-21811:** add derrived eta ctx ([e05f1da](https://github.com/bitdefender/dex-core/commit/e05f1da32aa29d36ebaea86ae6fe0fa852b18a4b))
|
|
22
|
+
* **DEX-21811:** add fallback ([ce1fe67](https://github.com/bitdefender/dex-core/commit/ce1fe676ef7101f97fe777b6cb5ee83b35ad5dce))
|
|
23
|
+
* **DEX-21811:** add parsers and renders ([0d52ebc](https://github.com/bitdefender/dex-core/commit/0d52ebcd3f73024cd2076406adbc8002a601264f))
|
|
24
|
+
* **DEX-21811:** add trial links ([209c577](https://github.com/bitdefender/dex-core/commit/209c577f6809023b2d73dc2783a4699453e6466f))
|
|
25
|
+
* **DEX-21811:** change overrides and trialinks ([db527b4](https://github.com/bitdefender/dex-core/commit/db527b495601d46857e9362f79dba8c033ffd665))
|
|
26
|
+
* **DEX-21811:** working click events ([9f1538f](https://github.com/bitdefender/dex-core/commit/9f1538f17d639381580eecfc1954bb16654a10d8))
|
|
27
|
+
* **DEX-2181:** add renders ([5175d15](https://github.com/bitdefender/dex-core/commit/5175d15772607be0afc2e8d6504417988e7dc5cf))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* **DEX-21811:** change state compute to transiztion bundles ([45dd085](https://github.com/bitdefender/dex-core/commit/45dd0858cf8a751236a29cad88dad6a5c1a16aa8))
|
|
33
|
+
* **DEX-21811:** fix bundle computing ([4f06ca4](https://github.com/bitdefender/dex-core/commit/4f06ca430ccd3d2e90ed69fdd57f600f31c77b6b))
|
|
34
|
+
* **DEX-21811:** fix bundle empty array ([79187b7](https://github.com/bitdefender/dex-core/commit/79187b73d68a124244e947eb60bf1a58549365c1))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
6
38
|
## [0.3.0](https://github.com/bitdefender/dex-core/compare/@repobit/dex-store@0.2.1...@repobit/dex-store@0.3.0) (2025-05-29)
|
|
7
39
|
|
|
8
40
|
|
package/README.md
CHANGED
|
@@ -1,11 +1,244 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @repobit/dex-store
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed, framework‑agnostic pricing/store core with pluggable providers, rich product/option models, and convenient helpers for formatting, overrides, and experimentation.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Fetch products/options via built‑in providers (init, vlaicu) or your own
|
|
6
|
+
- Strongly typed `Product` and `ProductOption` APIs
|
|
7
|
+
- Locale/currency formatting with overrideable formatter
|
|
8
|
+
- Option overrides and campaign resolver hooks
|
|
9
|
+
- Trial link mapping and buy‑link transformers
|
|
6
10
|
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @repobit/dex-store
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## At a glance
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Store } from '@repobit/dex-store';
|
|
21
|
+
|
|
22
|
+
const store = new Store({
|
|
23
|
+
locale : 'en-us',
|
|
24
|
+
provider: { name: 'vlaicu' },
|
|
25
|
+
|
|
26
|
+
// Optional async campaign resolver per product id
|
|
27
|
+
campaign: async ({ id, campaign }) => campaign,
|
|
28
|
+
|
|
29
|
+
// Optional per-variation overrides (keyed as `${devices}-${subscription}`)
|
|
30
|
+
overrides: {
|
|
31
|
+
'com.bitdefender.tsmd.v2': {
|
|
32
|
+
campaign: 'WINTER2025',
|
|
33
|
+
options : {
|
|
34
|
+
'5-12': { discountedPrice: 49.99 },
|
|
35
|
+
// delete a variation by setting null
|
|
36
|
+
'1-1' : null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Optional mapping of trial links per variation
|
|
42
|
+
trialLinks: {
|
|
43
|
+
'5-12': 'https://example.test/trial?devices=5&months=12'
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Optional buy-link transformer for A/B, UTM, etc.
|
|
47
|
+
transformers: {
|
|
48
|
+
option: {
|
|
49
|
+
buyLink: async (href) => new URL(href).toString()
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Optional number -> string formatter (defaults to Intl currency)
|
|
54
|
+
formatter: ({ price, currency, locale }) => {
|
|
55
|
+
if (!currency) return price; // return number when currency missing
|
|
56
|
+
return new Intl.NumberFormat(locale ?? 'en-us', { style: 'currency', currency }).format(price);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const product = await store.getProduct({ id: 'com.bitdefender.tsmd.v2' });
|
|
61
|
+
const option = await product?.getOption({ devices: 5, subscription: 12 });
|
|
62
|
+
|
|
63
|
+
console.log({
|
|
64
|
+
price : option?.getPrice(), // "$109.99"
|
|
65
|
+
discounted : option?.getDiscountedPrice(), // "$59.99"
|
|
66
|
+
discount : option?.getDiscount(), // "$50"
|
|
67
|
+
buyLink : option?.getBuyLink(),
|
|
68
|
+
trialLink : option?.getTrialLink(),
|
|
69
|
+
devices : option?.getDevices(), // 5
|
|
70
|
+
subscription : option?.getSubscription() // 12
|
|
71
|
+
});
|
|
7
72
|
```
|
|
8
|
-
import dexStore from '@bitdefender/dex-store';
|
|
9
73
|
|
|
10
|
-
|
|
74
|
+
## Store
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
new Store(config: {
|
|
78
|
+
provider : { name: 'init' | 'vlaicu' } | new (...args) => Provider,
|
|
79
|
+
locale : `${string}-${string}`,
|
|
80
|
+
campaign? : ({ id, campaign }: { id: string; campaign?: string }) => Promise<string | undefined>,
|
|
81
|
+
overrides? : {
|
|
82
|
+
[productId: string]: {
|
|
83
|
+
campaign?: string,
|
|
84
|
+
options : { [variation: `${string}-${string}`]: Partial<UnboundProductOptionData> | null | undefined }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
trialLinks?: { [variation: `${string}-${string}`]: string },
|
|
88
|
+
transformers?: { option?: { buyLink: (href: string) => Promise<string> } },
|
|
89
|
+
formatter? : (p: { price: number; currency?: string; locale?: Intl.UnicodeBCP47LocaleIdentifier }) => string | number
|
|
90
|
+
})
|
|
11
91
|
```
|
|
92
|
+
|
|
93
|
+
- `getProduct(selector)` returns a `Product | undefined` (or an array if you pass an array of selectors).
|
|
94
|
+
- Built‑in providers:
|
|
95
|
+
- `init`: legacy Init Selector
|
|
96
|
+
- `vlaicu`: platform API v1
|
|
97
|
+
- 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.
|
|
98
|
+
|
|
99
|
+
### Price formatting
|
|
100
|
+
- The store exposes a `formatter` hook. Defaults to Intl currency via `formatPrice`.
|
|
101
|
+
- All string price/discount getters (`getPrice()`, `getDiscountedPrice()`, `getDiscount()`) call `store.formatPrice` under the hood when returning strings.
|
|
102
|
+
- If you pass `{ currency: false }`/`{ symbol: false }`, the numeric variants are returned.
|
|
103
|
+
|
|
104
|
+
## Product
|
|
105
|
+
|
|
106
|
+
Represents a product with many options (variations). Computes min/max aggregates for price and discount across all options (including monthly breakdowns).
|
|
107
|
+
|
|
108
|
+
Key methods:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
product.getId(): string
|
|
112
|
+
product.getName(): string
|
|
113
|
+
product.getCampaign(): string | undefined
|
|
114
|
+
product.getCurrency(): string
|
|
115
|
+
product.getOption(): Promise<ProductOption[]> // all options
|
|
116
|
+
product.getOption({ devices, subscription }): Promise<ProductOption | undefined>
|
|
117
|
+
product.getPrice({ monthly?: boolean, currency?: boolean }): { min: string|number, max: string|number }
|
|
118
|
+
product.getDiscountedPrice({ monthly?: boolean, currency?: boolean }): { min: string|number, max: string|number }
|
|
119
|
+
product.getDiscount({ percentage?: boolean, symbol?: boolean }): { min: string|number, max: string|number }
|
|
120
|
+
product.getDevices(): { min: number, max: number, values: number[] }
|
|
121
|
+
product.getSubscriptions(): { min: number, max: number, values: number[] }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## ProductOption
|
|
125
|
+
|
|
126
|
+
A single product variation.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
option.getVariation(): `${devices}-${subscription}`
|
|
130
|
+
option.getDevices(): number
|
|
131
|
+
option.getSubscription(): number
|
|
132
|
+
|
|
133
|
+
// Price
|
|
134
|
+
option.getPrice({ monthly?: boolean, currency?: boolean }): string | number
|
|
135
|
+
option.getDiscountedPrice({ monthly?: boolean, currency?: boolean }): string | number
|
|
136
|
+
option.getDiscount({ percentage?: boolean, symbol?: boolean, monthly?: boolean }): string | number
|
|
137
|
+
|
|
138
|
+
// Links
|
|
139
|
+
option.getBuyLink(): string
|
|
140
|
+
option.getTrialLink(): string
|
|
141
|
+
|
|
142
|
+
// Navigate
|
|
143
|
+
option.getOption({ devices?: number; subscription?: number }): Promise<ProductOption | undefined>
|
|
144
|
+
option.nextOption({ devices?: 'prev' | 'next' | number; subscription?: 'prev' | 'next' | number }): Promise<ProductOption | undefined>
|
|
145
|
+
|
|
146
|
+
// Bundling
|
|
147
|
+
option.toogleBundle({ option: ProductBundleOption; devicesFixed?: boolean; subscriptionFixed?: boolean }): Promise<ProductOption | undefined>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Notes:
|
|
151
|
+
- String getters use the store’s formatter; numeric variants are returned when asked via `{ currency: false }` or `{ symbol: false }`.
|
|
152
|
+
- `nextOption` accepts deltas per dimension. `'next'/'prev'` move by one index. Numeric deltas are relative.
|
|
153
|
+
|
|
154
|
+
## Overrides and trial links
|
|
155
|
+
|
|
156
|
+
- `overrides` mutate/patch individual options before instantiation. Set a variation to `null` to drop it.
|
|
157
|
+
- `trialLinks` inject per‑variation trial links as plain strings. Providers can still supply a trial link; the mapping overwrites it when present.
|
|
158
|
+
- `transformers.option.buyLink` lets you rewrite buy links uniformly (e.g., add UTM, swap host).
|
|
159
|
+
|
|
160
|
+
## Custom providers
|
|
161
|
+
|
|
162
|
+
Implement your own by extending the abstract `Provider`:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { Provider } from '@repobit/dex-store/src/providers/provider.base';
|
|
166
|
+
import type { UnboundProductData } from '@repobit/dex-store/src/products/product.base';
|
|
167
|
+
|
|
168
|
+
class MyProvider extends Provider {
|
|
169
|
+
async fetch({ id, campaign }): Promise<UnboundProductData | undefined> {
|
|
170
|
+
// call your API, adapt { devices, subscription } using this.adaptTo(...)
|
|
171
|
+
return {
|
|
172
|
+
id,
|
|
173
|
+
name: 'My Product',
|
|
174
|
+
currency: 'USD',
|
|
175
|
+
campaign,
|
|
176
|
+
campaignType: '',
|
|
177
|
+
platformId: 'P123',
|
|
178
|
+
options: new Map([
|
|
179
|
+
['5-12', {
|
|
180
|
+
devices: 5,
|
|
181
|
+
subscription: 12,
|
|
182
|
+
price: 109.99,
|
|
183
|
+
discountedPrice: 59.99,
|
|
184
|
+
buyLink: 'https://example/order',
|
|
185
|
+
trialLink: 'https://example/trial',
|
|
186
|
+
bundle: []
|
|
187
|
+
}]
|
|
188
|
+
])
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const store = new Store({ locale: 'en-us', provider: MyProvider });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Formatting helper
|
|
197
|
+
|
|
198
|
+
`formatPrice({ price, currency, locale })` is exported for convenience. It uses `Intl.NumberFormat` and returns a string when `currency` is provided, otherwise the raw number.
|
|
199
|
+
|
|
200
|
+
## Caching and aggregation
|
|
201
|
+
|
|
202
|
+
- The store caches results per adapted product id + campaign. The adapter can normalize/alias ids before the fetch.
|
|
203
|
+
- If you request the same logical product using multiple raw ids that adapt to the same platform id, the store aggregates options:
|
|
204
|
+
- First fetch stores a base Product instance in the cache.
|
|
205
|
+
- Later fetches for the same cache key merge their options into the base via `SET_OPTION`, so you see a superset of options.
|
|
206
|
+
- This is helpful when product variations are delivered under multiple source ids that map to a single platform product.
|
|
207
|
+
|
|
208
|
+
## Built‑in providers
|
|
209
|
+
|
|
210
|
+
- `init` (Init Selector)
|
|
211
|
+
- Posts a small JSON payload to Bitdefender’s selector endpoint, returns a nested map of variations.
|
|
212
|
+
- Computes buy links, adapts `{ devices, subscription }`, and extracts discounted price when available.
|
|
213
|
+
- Respects the `campaign` resolver and an internal ignore list (to disable promotions).
|
|
214
|
+
|
|
215
|
+
- `vlaicu` (Platform API v1)
|
|
216
|
+
- Fetches a flat list of options with `{ slots, months, price, discountedPrice, buyLink }`.
|
|
217
|
+
- Adapts `{ devices, subscription }` using the store adapter.
|
|
218
|
+
- Uses store locale for currency formatting.
|
|
219
|
+
|
|
220
|
+
Both providers:
|
|
221
|
+
- Respect `overrides`, `trialLinks` and `transformers.option.buyLink`.
|
|
222
|
+
- Use the store’s adapter for id/variation normalization.
|
|
223
|
+
|
|
224
|
+
## Troubleshooting
|
|
225
|
+
|
|
226
|
+
- Price shows as a number instead of a formatted string
|
|
227
|
+
- Ensure the product provides a currency, or pass a `formatter` in Store config.
|
|
228
|
+
|
|
229
|
+
- `getOption`/`nextOption` returns `undefined`
|
|
230
|
+
- The requested `{ devices, subscription }` is outside the product’s min/max or not present in the values list.
|
|
231
|
+
|
|
232
|
+
- Overrides had no effect
|
|
233
|
+
- Check the `overrides` key matches the product id returned by the provider (after adaptation).
|
|
234
|
+
- Ensure variation keys are `${devices}-${subscription}` strings.
|
|
235
|
+
|
|
236
|
+
- Trial link isn’t set
|
|
237
|
+
- Confirm `trialLinks` has an entry for the variation and not overwritten later by the provider.
|
|
238
|
+
|
|
239
|
+
- Different ids return a single product
|
|
240
|
+
- That’s expected when ids adapt to the same platform id; options are aggregated into one Product instance.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
ISC
|
|
@@ -9,23 +9,23 @@ export class Adaptor {
|
|
|
9
9
|
}
|
|
10
10
|
async adaptTo(param) {
|
|
11
11
|
const { id, devices, subscription } = param;
|
|
12
|
-
const product = { id };
|
|
12
|
+
const product = { id, devices, subscription };
|
|
13
13
|
if (!this.mapper) {
|
|
14
|
-
product.devices = devices;
|
|
15
|
-
product.subscription = subscription;
|
|
16
14
|
return product;
|
|
17
15
|
}
|
|
18
16
|
const mappedProduct = this.mapper.get(id);
|
|
19
17
|
if (mappedProduct) {
|
|
18
|
+
// Use the canonical adapted id for providers/consumers
|
|
20
19
|
product.id = mappedProduct.id;
|
|
21
20
|
}
|
|
22
21
|
else {
|
|
23
|
-
|
|
24
|
-
product.subscription = subscription;
|
|
22
|
+
// No id mapping known; return as-is
|
|
25
23
|
return product;
|
|
26
24
|
}
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// Ensure variation mapping is loaded using the source id (original mapping key)
|
|
26
|
+
if (!mappedProduct.options) {
|
|
27
|
+
const sheetId = mappedProduct.sourceId || id;
|
|
28
|
+
mappedProduct.options = this.getMappingForId(sheetId);
|
|
29
29
|
}
|
|
30
30
|
if (devices && subscription) {
|
|
31
31
|
const options = await mappedProduct.options;
|
|
@@ -50,7 +50,9 @@ export class Adaptor {
|
|
|
50
50
|
const products = new Map();
|
|
51
51
|
for (const product of sheet.data) {
|
|
52
52
|
const { from, to } = product;
|
|
53
|
-
|
|
53
|
+
// Map both the old id and the new id to the same canonical id and source sheet
|
|
54
|
+
products.set(from, { id: to, sourceId: from });
|
|
55
|
+
products.set(to, { id: to, sourceId: from });
|
|
54
56
|
}
|
|
55
57
|
return products;
|
|
56
58
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adaptor.base.js","sourceRoot":"","sources":["../../../src/adaptors/adaptor.base.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adaptor.base.js","sourceRoot":"","sources":["../../../src/adaptors/adaptor.base.ts"],"names":[],"mappings":"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"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ProductOption } from "./product-options/option.base.js";
|
|
1
|
+
import { ProductBundleOption, ProductOption } from "./product-options/option.base.js";
|
|
2
2
|
import { Product } from "./products/product.base.js";
|
|
3
3
|
import { Store } from "./store.js";
|
|
4
4
|
export { Product, ProductOption, Store };
|
|
5
|
+
export type { ProductBundleOption };
|
package/dist/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,OAAO,EACL,OAAO,EAAE,aAAa,EAAE,KAAK,EAC9B,CAAC"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Product } from "../products/product.base.js";
|
|
2
|
+
import { ProductSelector } from "../store.js";
|
|
3
|
+
type GetOption = number | "prev" | "next";
|
|
2
4
|
export type ProductOptionData = {
|
|
3
5
|
product: Product;
|
|
4
6
|
price: number;
|
|
@@ -6,6 +8,13 @@ export type ProductOptionData = {
|
|
|
6
8
|
devices: number;
|
|
7
9
|
subscription: number;
|
|
8
10
|
buyLink: string;
|
|
11
|
+
trialLink?: string;
|
|
12
|
+
bundle?: ProductBundleOption[];
|
|
13
|
+
};
|
|
14
|
+
export type ProductBundleOption = {
|
|
15
|
+
option: ProductOption;
|
|
16
|
+
devicesFixed?: boolean;
|
|
17
|
+
subscriptionFixed?: boolean;
|
|
9
18
|
};
|
|
10
19
|
export declare class ProductOption {
|
|
11
20
|
private product;
|
|
@@ -15,38 +24,61 @@ export declare class ProductOption {
|
|
|
15
24
|
private subscription;
|
|
16
25
|
private currency?;
|
|
17
26
|
private buyLink;
|
|
27
|
+
private trialLink?;
|
|
18
28
|
private discount;
|
|
29
|
+
private bundle;
|
|
19
30
|
constructor(option: ProductOptionData);
|
|
20
31
|
getProduct(): Product;
|
|
21
32
|
getVariation(): string;
|
|
22
33
|
getPrice(): string;
|
|
23
34
|
getPrice(param: {
|
|
24
35
|
monthly?: boolean;
|
|
25
|
-
currency
|
|
36
|
+
currency?: true;
|
|
26
37
|
}): string;
|
|
27
38
|
getPrice(param: {
|
|
28
39
|
monthly?: boolean;
|
|
29
|
-
currency
|
|
40
|
+
currency: false;
|
|
30
41
|
}): number;
|
|
31
42
|
getDiscountedPrice(): string;
|
|
32
43
|
getDiscountedPrice(param: {
|
|
33
44
|
monthly?: boolean;
|
|
34
|
-
currency
|
|
45
|
+
currency?: true;
|
|
35
46
|
}): string;
|
|
36
47
|
getDiscountedPrice(param: {
|
|
37
48
|
monthly?: boolean;
|
|
38
|
-
currency
|
|
49
|
+
currency: false;
|
|
39
50
|
}): number;
|
|
40
|
-
getDiscount():
|
|
51
|
+
getDiscount(): string;
|
|
41
52
|
getDiscount(param: {
|
|
42
53
|
percentage?: boolean;
|
|
43
|
-
symbol
|
|
54
|
+
symbol?: true;
|
|
55
|
+
monthly?: boolean;
|
|
44
56
|
}): string;
|
|
45
57
|
getDiscount(param: {
|
|
46
58
|
percentage?: boolean;
|
|
47
|
-
symbol
|
|
59
|
+
symbol: false;
|
|
60
|
+
monthly?: boolean;
|
|
48
61
|
}): number;
|
|
49
62
|
getBuyLink(): string;
|
|
50
63
|
getDevices(): number;
|
|
51
64
|
getSubscription(): number;
|
|
65
|
+
getBundle(): ProductBundleOption[];
|
|
66
|
+
getTrialLink(): string | undefined;
|
|
67
|
+
getOption(option: {
|
|
68
|
+
devices?: number;
|
|
69
|
+
subscription?: number;
|
|
70
|
+
}): Promise<ProductOption | undefined>;
|
|
71
|
+
nextOption(option: {
|
|
72
|
+
devices: GetOption;
|
|
73
|
+
subscription?: GetOption;
|
|
74
|
+
} | {
|
|
75
|
+
devices?: GetOption;
|
|
76
|
+
subscription: GetOption;
|
|
77
|
+
} | {
|
|
78
|
+
devices: GetOption;
|
|
79
|
+
subscription: GetOption;
|
|
80
|
+
}): Promise<ProductOption | undefined>;
|
|
81
|
+
toogleBundle(bundle: ProductBundleOption): Promise<ProductOption | undefined>;
|
|
82
|
+
switchProduct(productSelector: ProductSelector): Promise<ProductOption | undefined>;
|
|
52
83
|
}
|
|
84
|
+
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { formatPrice } from "../format-price.js";
|
|
2
1
|
export class ProductOption {
|
|
3
2
|
product;
|
|
4
3
|
price;
|
|
@@ -7,7 +6,9 @@ export class ProductOption {
|
|
|
7
6
|
subscription;
|
|
8
7
|
currency;
|
|
9
8
|
buyLink;
|
|
9
|
+
trialLink;
|
|
10
10
|
discount;
|
|
11
|
+
bundle;
|
|
11
12
|
constructor(option) {
|
|
12
13
|
this.product = option.product;
|
|
13
14
|
this.price = {
|
|
@@ -22,10 +23,16 @@ export class ProductOption {
|
|
|
22
23
|
this.subscription = option.subscription;
|
|
23
24
|
this.currency = option.product.getCurrency();
|
|
24
25
|
this.buyLink = option.buyLink;
|
|
26
|
+
this.trialLink = option.trialLink;
|
|
25
27
|
this.discount = {
|
|
26
28
|
value: Math.round((option.price - option.discountedPrice + Number.EPSILON) * 100) / 100,
|
|
27
|
-
percentage: Math.round(((option.price - option.discountedPrice) / option.price * 100))
|
|
29
|
+
percentage: Math.round(((option.price - option.discountedPrice) / option.price * 100)),
|
|
30
|
+
monthly: {
|
|
31
|
+
value: Math.round((option.price - option.discountedPrice + Number.EPSILON) * 100 / option.subscription) / 100,
|
|
32
|
+
percentage: Math.round(((option.price - option.discountedPrice) / option.price * 100 / option.subscription))
|
|
33
|
+
}
|
|
28
34
|
};
|
|
35
|
+
this.bundle = option.bundle || [];
|
|
29
36
|
}
|
|
30
37
|
getProduct() {
|
|
31
38
|
return this.product;
|
|
@@ -37,7 +44,7 @@ export class ProductOption {
|
|
|
37
44
|
const { monthly = false, currency = true } = param ?? {};
|
|
38
45
|
const rawPrice = monthly ? this.price.monthly : this.price.value;
|
|
39
46
|
if (currency) {
|
|
40
|
-
return formatPrice({ price: rawPrice, currency: this.currency });
|
|
47
|
+
return this.product.getStore().formatPrice({ price: rawPrice, currency: this.currency });
|
|
41
48
|
}
|
|
42
49
|
return rawPrice;
|
|
43
50
|
}
|
|
@@ -45,17 +52,18 @@ export class ProductOption {
|
|
|
45
52
|
const { monthly = false, currency = true } = param ?? {};
|
|
46
53
|
const rawPrice = monthly ? this.discountedPrice.monthly : this.discountedPrice.value;
|
|
47
54
|
if (currency) {
|
|
48
|
-
return formatPrice({ price: rawPrice, currency: this.currency });
|
|
55
|
+
return this.product.getStore().formatPrice({ price: rawPrice, currency: this.currency });
|
|
49
56
|
}
|
|
50
57
|
return rawPrice;
|
|
51
58
|
}
|
|
52
59
|
getDiscount(param) {
|
|
53
|
-
const { percentage = false, symbol = true } = param ?? {};
|
|
54
|
-
const
|
|
60
|
+
const { percentage = false, symbol = true, monthly = false } = param ?? {};
|
|
61
|
+
const discount = monthly ? this.discount.monthly : this.discount;
|
|
62
|
+
const rawValue = percentage ? discount.percentage : discount.value;
|
|
55
63
|
if (symbol) {
|
|
56
64
|
return percentage
|
|
57
65
|
? `${rawValue}%`
|
|
58
|
-
: formatPrice({ price: rawValue, currency: this.currency });
|
|
66
|
+
: this.product.getStore().formatPrice({ price: rawValue, currency: this.currency });
|
|
59
67
|
}
|
|
60
68
|
return rawValue;
|
|
61
69
|
}
|
|
@@ -68,5 +76,136 @@ export class ProductOption {
|
|
|
68
76
|
getSubscription() {
|
|
69
77
|
return this.subscription;
|
|
70
78
|
}
|
|
79
|
+
getBundle() {
|
|
80
|
+
return this.bundle;
|
|
81
|
+
}
|
|
82
|
+
getTrialLink() {
|
|
83
|
+
return this.trialLink;
|
|
84
|
+
}
|
|
85
|
+
async getOption(option) {
|
|
86
|
+
if (!option) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const devices = this.product.getDevices();
|
|
90
|
+
const subscriptions = this.product.getSubscriptions();
|
|
91
|
+
// Determine target indices for devices and subscriptions
|
|
92
|
+
const computeIndex = (currentIndex, values, opt) => {
|
|
93
|
+
if (opt === undefined) {
|
|
94
|
+
// no change requested
|
|
95
|
+
return currentIndex;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// numeric means absolute index value
|
|
99
|
+
const numeric = Number(opt);
|
|
100
|
+
return values.findIndex(v => v === numeric);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// Current indices
|
|
104
|
+
const idxDevice = devices.values.findIndex(v => v === this.devices);
|
|
105
|
+
const idxSubscription = subscriptions.values.findIndex(v => v === this.subscription);
|
|
106
|
+
// Next indices
|
|
107
|
+
const targetDeviceIdx = computeIndex(idxDevice, devices.values, option.devices);
|
|
108
|
+
const targetSubscriptionIdx = computeIndex(idxSubscription, subscriptions.values, option.subscription);
|
|
109
|
+
// Calculate new values
|
|
110
|
+
const newDevice = devices.values[targetDeviceIdx];
|
|
111
|
+
const newSubscription = subscriptions.values[targetSubscriptionIdx];
|
|
112
|
+
// Bail out if out of range
|
|
113
|
+
if (targetDeviceIdx < 0 ||
|
|
114
|
+
targetSubscriptionIdx < 0 ||
|
|
115
|
+
newDevice > devices.max ||
|
|
116
|
+
newSubscription > subscriptions.max ||
|
|
117
|
+
newDevice < devices.min ||
|
|
118
|
+
newSubscription < devices.min) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
// Build new bundle, mapping fixed options and fetching new ones
|
|
122
|
+
const newBundle = await Promise.all(this.bundle.map(async (b) => {
|
|
123
|
+
const nextOption = await b.option.getOption({
|
|
124
|
+
devices: b.devicesFixed ? b.option.getDevices() : option.devices,
|
|
125
|
+
subscription: b.subscriptionFixed ? b.option.getSubscription() : option.subscription
|
|
126
|
+
});
|
|
127
|
+
return { ...b, option: nextOption };
|
|
128
|
+
}));
|
|
129
|
+
// Ensure all bundle options are defined
|
|
130
|
+
const isDefinedBundle = (b) => b.option != null;
|
|
131
|
+
if (!newBundle.every(isDefinedBundle)) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
// Return the new ProductOption
|
|
135
|
+
return this.product.getOption({
|
|
136
|
+
devices: newDevice,
|
|
137
|
+
subscription: newSubscription,
|
|
138
|
+
bundle: newBundle
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async nextOption(option) {
|
|
142
|
+
if (!option) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const devices = this.product.getDevices();
|
|
146
|
+
const subscriptions = this.product.getSubscriptions();
|
|
147
|
+
// Determine target indices for devices and subscriptions
|
|
148
|
+
const computeIndex = (currentIndex, values, opt) => {
|
|
149
|
+
if (opt === undefined) {
|
|
150
|
+
// no change requested
|
|
151
|
+
return currentIndex;
|
|
152
|
+
}
|
|
153
|
+
else if (opt === 'next') {
|
|
154
|
+
return currentIndex + 1;
|
|
155
|
+
}
|
|
156
|
+
else if (opt === 'prev') {
|
|
157
|
+
return currentIndex - 1;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const numeric = currentIndex + Number(opt);
|
|
161
|
+
return values.findIndex(v => v === numeric);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
// Current indices
|
|
165
|
+
const idxDevice = devices.values.findIndex(v => v === this.devices);
|
|
166
|
+
const idxSubscription = subscriptions.values.findIndex(v => v === this.subscription);
|
|
167
|
+
// Next indices
|
|
168
|
+
const targetDeviceIdx = computeIndex(idxDevice, devices.values, option.devices);
|
|
169
|
+
const targetSubscriptionIdx = computeIndex(idxSubscription, subscriptions.values, option.subscription);
|
|
170
|
+
// Calculate new values
|
|
171
|
+
const newDevice = devices.values[targetDeviceIdx];
|
|
172
|
+
const newSubscription = subscriptions.values[targetSubscriptionIdx];
|
|
173
|
+
// Bail out if out of range
|
|
174
|
+
if (targetDeviceIdx < 0 ||
|
|
175
|
+
targetSubscriptionIdx < 0 ||
|
|
176
|
+
newDevice > devices.max ||
|
|
177
|
+
newSubscription > subscriptions.max ||
|
|
178
|
+
newDevice < devices.min ||
|
|
179
|
+
newSubscription < devices.min) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
return this.getOption({ devices: newDevice, subscription: newSubscription });
|
|
183
|
+
}
|
|
184
|
+
async toogleBundle(bundle) {
|
|
185
|
+
const getKey = (option) => {
|
|
186
|
+
return `${option.getProduct().getId()}${option.getProduct().getCampaign()}${option.getDevices()}${option.getSubscription()}`;
|
|
187
|
+
};
|
|
188
|
+
const idx = this.bundle.findIndex(b => getKey(b.option) === getKey(bundle.option));
|
|
189
|
+
let newBundle;
|
|
190
|
+
if (idx !== -1) {
|
|
191
|
+
newBundle = this.bundle.toSpliced(idx, 1);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
newBundle = this.bundle.toSpliced(0, 0, bundle);
|
|
195
|
+
}
|
|
196
|
+
return this.product.getOption({
|
|
197
|
+
devices: this.devices,
|
|
198
|
+
subscription: this.subscription,
|
|
199
|
+
bundle: newBundle
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async switchProduct(productSelector) {
|
|
203
|
+
const product = await this.product.getStore().getProduct(productSelector);
|
|
204
|
+
return product?.getOption({
|
|
205
|
+
devices: this.devices,
|
|
206
|
+
subscription: this.subscription,
|
|
207
|
+
bundle: this.bundle
|
|
208
|
+
});
|
|
209
|
+
}
|
|
71
210
|
}
|
|
72
211
|
//# sourceMappingURL=option.base.js.map
|