@kelviq/js-promotions-ui 0.0.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.
@@ -0,0 +1,62 @@
1
+ import { PricingData } from "./types";
2
+
3
+ export const API_URL = import.meta.env.VITE_API_URL;
4
+
5
+ export const SANDBOX_API_URL = import.meta.env.VITE_SANDBOX_API_URL;
6
+
7
+ export const ERROR_MSG =
8
+ "Oops! Something went wrong. Please email hi@kelviq.com";
9
+
10
+ export interface FetchPricingDataOptions {
11
+ promotionId?: string;
12
+ environment?: string;
13
+ accessToken?: string;
14
+ }
15
+
16
+ export function fetchPricingData(
17
+ config: FetchPricingDataOptions
18
+ ): Promise<PricingData | null> {
19
+ return new Promise(function (resolve, reject) {
20
+ const request = new XMLHttpRequest();
21
+
22
+ const searchParams = new URLSearchParams();
23
+
24
+ const { promotionId, environment = "production", accessToken } = config;
25
+
26
+ if (promotionId) {
27
+ searchParams.append("promotion_id", promotionId);
28
+ }
29
+
30
+ const params = searchParams.toString();
31
+
32
+ const apiUrl = environment === "sandbox" ? SANDBOX_API_URL : API_URL;
33
+
34
+ request.open("GET", `${apiUrl}?${params}`, true);
35
+
36
+ if (accessToken) {
37
+ request.setRequestHeader("Authorization", `Bearer ${accessToken}`);
38
+ }
39
+
40
+ request.onload = function () {
41
+ if (this.status >= 200 && this.status < 400) {
42
+ try {
43
+ const data = JSON.parse(this.response);
44
+ resolve(data);
45
+ } catch (error) {
46
+ console.error("Error parsing API response:", error);
47
+ reject(new Error("Failed to parse API response"));
48
+ }
49
+ } else {
50
+ console.error(ERROR_MSG, this.status);
51
+ reject(new Error("API request failed with status " + this.status));
52
+ }
53
+ };
54
+
55
+ request.onerror = function () {
56
+ console.error("Network error occurred");
57
+ reject(new Error("Network error occurred"));
58
+ };
59
+
60
+ request.send();
61
+ });
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { KQPromotionUISDK } from "./sdk";
2
+ // entry point for the package
@@ -0,0 +1,44 @@
1
+ function resolvePdSDKFunction(e, ...t) {
2
+ return new Promise((n, i) => {
3
+ !(function r() {
4
+ window.KQPromotionUISDK && "function" == typeof window.KQPromotionUISDK[e]
5
+ ? window.KQPromotionUISDK[e](...t)
6
+ .then(n)
7
+ .catch(i)
8
+ : setTimeout(r, 100);
9
+ })();
10
+ });
11
+ }
12
+ (!(function (e, t, n, i, r, a, c) {
13
+ ((e[i] =
14
+ e[i] ||
15
+ function () {
16
+ (e[i].q = e[i].q || []).push(Array.prototype.slice.call(arguments));
17
+ }),
18
+ (a = t.createElement(n)),
19
+ (c = t.getElementsByTagName(n)[0]),
20
+ (a.id = "kelviq-sdk"),
21
+ (a.async = 1),
22
+ (a.src = r),
23
+ c.parentNode.insertBefore(a, c));
24
+ })(
25
+ window,
26
+ document,
27
+ "script",
28
+ "KQPromotionUISDK",
29
+ "https://cdn.kelviq.com/js-promotions-ui/0.0.1/js-promotions-ui.umd.js"
30
+ ),
31
+ (window.KQPromotionUI = {
32
+ init: function (e) {
33
+ KQPromotionUISDK("init", e);
34
+ },
35
+ getUpdatedPrice: function (e, t) {
36
+ return resolvePdSDKFunction("getUpdatedPrice", e, t);
37
+ },
38
+ updatePriceElement: function (e, t) {
39
+ return resolvePdSDKFunction("updatePriceElement", e, t);
40
+ },
41
+ updatePrice: function (e) {
42
+ return resolvePdSDKFunction("updatePrice", e);
43
+ },
44
+ }));
package/src/sdk.ts ADDED
@@ -0,0 +1,504 @@
1
+ import { findRelatedElements } from "./dom-utils";
2
+ import { fetchPricingData } from "./http-utils";
3
+ import { DefaultConfig, PricingData, PriceObject } from "./types";
4
+ import { CurrencyDisplay, DEFAULT_CONFIG, pdFormatCurrency } from "./utils";
5
+
6
+ interface KQPromotionUISDK {
7
+ init: (options: Partial<DefaultConfig>) => Promise<PricingData | null>;
8
+ getUpdatedPrice: (
9
+ price: number,
10
+ options?: Partial<DefaultConfig>
11
+ ) => Promise<PriceObject | undefined>;
12
+ updatePriceElement: (
13
+ element: HTMLElement,
14
+ options?: Partial<DefaultConfig>
15
+ ) => Promise<PriceObject | undefined>;
16
+ updatePrice: (
17
+ priceConfigs: {
18
+ element: HTMLElement;
19
+ price: number;
20
+ options?: Partial<DefaultConfig>;
21
+ }[]
22
+ ) => unknown;
23
+ }
24
+
25
+ export const KQPromotionUISDK = function (window: Window, document: Document) {
26
+ // Default configuration
27
+
28
+ let pricingData: PricingData | null = null;
29
+ let initPromise: Promise<PricingData | null> | null = null;
30
+ let config: DefaultConfig = { ...DEFAULT_CONFIG };
31
+
32
+ // Add this at the beginning to handle queued commands
33
+ const queue = (window as any).KQPromotionUISDK.q || [];
34
+
35
+ function mergeConfig(customConfig: Partial<DefaultConfig>) {
36
+ config = { ...DEFAULT_CONFIG, ...customConfig };
37
+ }
38
+
39
+ function updateBodyClasses() {
40
+ const hasDiscount = pricingData && pricingData.percentage;
41
+ document.body.classList.toggle("kq-discount-applied", !!hasDiscount);
42
+ }
43
+
44
+ function updatePrices() {
45
+ if (!pricingData) return;
46
+
47
+ updateBodyClasses();
48
+
49
+ const priceElements = document.querySelectorAll("[data-kq-price]");
50
+ for (let i = 0; i < priceElements.length; i++) {
51
+ const element = priceElements[i] as HTMLElement;
52
+ updatePriceElementInternal(element, false);
53
+
54
+ const relatedElements = findRelatedElements(element);
55
+ if (relatedElements) {
56
+ relatedElements.forEach(relatedElement => {
57
+ if (
58
+ relatedElement !== element &&
59
+ relatedElement.hasAttribute("data-kq-original-price-display")
60
+ ) {
61
+ updatePriceElementInternal(
62
+ relatedElement as HTMLElement,
63
+ true,
64
+ element
65
+ );
66
+ }
67
+ });
68
+ }
69
+ }
70
+ }
71
+
72
+ function getPriceOptions(element: HTMLElement): Partial<DefaultConfig> {
73
+ return {
74
+ showDecimal:
75
+ element.getAttribute("data-kq-show-decimal") !== null
76
+ ? element.getAttribute("data-kq-show-decimal") !== "false"
77
+ : config.showDecimal,
78
+ minimumDecimalDigits:
79
+ element.getAttribute("data-kq-minimum-decimal-digits") !== null
80
+ ? parseInt(element.getAttribute("data-kq-minimum-decimal-digits"))
81
+ : config.minimumDecimalDigits,
82
+ maximumDecimalDigits:
83
+ element.getAttribute("data-kq-maximum-decimal-digits") !== null
84
+ ? parseInt(element.getAttribute("data-kq-maximum-decimal-digits"))
85
+ : config.maximumDecimalDigits,
86
+ localizePricing:
87
+ element.getAttribute("data-kq-localize-pricing") !== null
88
+ ? element.getAttribute("data-kq-localize-pricing") === "true"
89
+ : config.localizePricing,
90
+ currencyDisplay: (element.getAttribute("data-kq-currency-display") !==
91
+ null
92
+ ? element.getAttribute("data-kq-currency-display")
93
+ : config.currencyDisplay) as CurrencyDisplay,
94
+ };
95
+ }
96
+
97
+ function updatePriceElementInternal(
98
+ element: HTMLElement,
99
+ isOriginalDisplay: boolean,
100
+ relatedPriceElement?: HTMLElement | null
101
+ ) {
102
+ const price = isOriginalDisplay
103
+ ? parseFloat(element.getAttribute("data-kq-original-price-display") || "")
104
+ : parseFloat(element.getAttribute("data-kq-price") || "");
105
+
106
+ if (isNaN(price)) {
107
+ console.error(
108
+ `Invalid ${isOriginalDisplay ? "original price display" : "original price"} for`,
109
+ element
110
+ );
111
+ return;
112
+ }
113
+
114
+ if (!pricingData) return;
115
+
116
+ const options = getPriceOptions(element);
117
+ let priceToDisplay = price;
118
+ let discountApplied = false;
119
+
120
+ if (!isOriginalDisplay) {
121
+ if (pricingData.percentage) {
122
+ priceToDisplay *= 1 - pricingData.percentage / 100;
123
+ discountApplied = true;
124
+ }
125
+ }
126
+
127
+ element.classList.toggle("kq-discount-applied", !!discountApplied);
128
+ element.classList.toggle("kq-original-price-display", isOriginalDisplay);
129
+ element.classList.toggle("kq-updated-price-display", !isOriginalDisplay);
130
+
131
+ updatePriceDisplay(element, priceToDisplay, options);
132
+
133
+ if (isOriginalDisplay && relatedPriceElement) {
134
+ const originalPrice = parseFloat(
135
+ relatedPriceElement.getAttribute("data-kq-price") || ""
136
+ );
137
+ let updatedPrice = originalPrice;
138
+
139
+ if (pricingData.percentage) {
140
+ updatedPrice *= 1 - pricingData.percentage / 100;
141
+ }
142
+
143
+ element.style.display = price > updatedPrice ? "" : "none";
144
+ }
145
+ }
146
+
147
+ function updatePriceDisplay(
148
+ container: HTMLElement,
149
+ price: number,
150
+ options: Partial<DefaultConfig>
151
+ ) {
152
+ if (!pricingData) return;
153
+ const displayPrice = price;
154
+
155
+ const mergedOptions = {
156
+ ...config,
157
+ ...options,
158
+ };
159
+
160
+ const currencyDisplay = mergedOptions.currencyDisplay as CurrencyDisplay;
161
+
162
+ const { formatted, integer, decimal, decimalSeparator } = pdFormatCurrency({
163
+ amount: displayPrice,
164
+ currency: mergedOptions.baseCurrencyCode,
165
+ locale: "en-US",
166
+ currencyDisplay,
167
+ formatStyle: "standard",
168
+ minimumFractionDigits: mergedOptions.showDecimal
169
+ ? mergedOptions.minimumDecimalDigits
170
+ : 0,
171
+ maximumFractionDigits: mergedOptions.showDecimal
172
+ ? mergedOptions.maximumDecimalDigits
173
+ : 0,
174
+ });
175
+
176
+ updateElements(
177
+ container,
178
+ "[data-kq-currency-symbol]",
179
+ mergedOptions.baseCurrencySymbol
180
+ );
181
+ updateElements(container, "[data-kq-price-integer]", integer);
182
+
183
+ if (mergedOptions.showDecimal) {
184
+ const decimalPart = decimal || "";
185
+ updateElements(container, "[data-kq-price-decimal]", decimalPart);
186
+ updateElements(
187
+ container,
188
+ "[data-kq-price-decimal-separator]",
189
+ decimalSeparator
190
+ );
191
+ } else {
192
+ updateElements(container, "[data-kq-price-decimal]", "");
193
+ updateElements(container, "[data-kq-price-decimal-separator]", "");
194
+ }
195
+
196
+ updateElements(
197
+ container,
198
+ "[data-kq-currency-code]",
199
+ mergedOptions.baseCurrencyCode
200
+ );
201
+ updateElements(container, "[data-kq-price-formatted]", formatted);
202
+ }
203
+
204
+ function updateElements(
205
+ container: HTMLElement,
206
+ selector: string,
207
+ value: string
208
+ ) {
209
+ const elements = container.querySelectorAll(selector);
210
+ for (let i = 0; i < elements.length; i++) {
211
+ elements[i].textContent = value;
212
+ }
213
+ }
214
+
215
+ function createBanner(data: PricingData) {
216
+ const bannerWidget = data.widgets?.find(w => w.type === "BANNER");
217
+ if (!bannerWidget || !config.showBanner) return;
218
+
219
+ const rawMessage = bannerWidget.countryMessage;
220
+ if (!rawMessage) return;
221
+
222
+ const bannerConfig = { ...bannerWidget, ...config.banner };
223
+
224
+ const savedStyles = !bannerConfig.unStyled
225
+ ? `.parity-banner {background-color: ${bannerConfig.backgroundColor || "#1a1a2e"};color: ${bannerConfig.fontColor || "#ffffff"};border-radius: ${bannerConfig.borderRadius || "0"};font-size: ${bannerConfig.fontSize || "14px"};padding: 20px 10px;text-align: center;position: relative;}`
226
+ : "";
227
+ const closeBtnStyles = bannerConfig.addCloseIcon
228
+ ? ".parity-banner-close-btn{width:1rem;height:1rem;border:0;opacity:.5;background-color:transparent;color:currentColor;position:absolute;top:1rem;right:1rem;padding:0}.parity-banner-close-btn:hover{opacity:1;}"
229
+ : ".parity-banner-close-btn{display:none;}";
230
+ const logoStyles =
231
+ ".parity-banner.parity-banner-has-logo {padding-left: 120px;}";
232
+
233
+ const styleSheet = document.createElement("style");
234
+ styleSheet.innerText = savedStyles + closeBtnStyles + logoStyles;
235
+ document.head.appendChild(styleSheet);
236
+ document.body.classList.add("kq-has-parity-banner");
237
+
238
+ const countryFlag = data.countryCode
239
+ ? data.countryCode
240
+ .toUpperCase()
241
+ .split("")
242
+ .map(c => String.fromCodePoint(c.charCodeAt(0) - 65 + 0x1f1e6))
243
+ .join("")
244
+ : "";
245
+
246
+ const resolvedMessage = rawMessage
247
+ .replace(/\{country_flag\}/g, countryFlag)
248
+ .replace(/\{country\}/g, data.country || "")
249
+ .replace(/\{coupon_code\}/g, data.code || "")
250
+ .replace(/\{discount_percentage\}/g, String(data.percentage || ""));
251
+
252
+ const bannerHTML =
253
+ `<div class="parity-banner">` +
254
+ `<button class="parity-banner-close-btn">×</button>` +
255
+ `<span>${resolvedMessage}</span></div>`;
256
+
257
+ const existingBanner = document.querySelector(".parity-banner");
258
+
259
+ if (!existingBanner) {
260
+ const container =
261
+ (bannerConfig.container &&
262
+ document.querySelector(bannerConfig.container)) ||
263
+ document.body;
264
+ if (bannerConfig.placement === "top") {
265
+ container.insertAdjacentHTML("afterbegin", bannerHTML);
266
+ } else {
267
+ container.insertAdjacentHTML("beforeend", bannerHTML);
268
+ }
269
+ }
270
+
271
+ if (bannerConfig.addCloseIcon) {
272
+ const closeBtn = document.querySelector(".parity-banner-close-btn");
273
+ if (closeBtn) {
274
+ function closeBannerHandler() {
275
+ const banner = document.querySelector(".parity-banner");
276
+ if (banner) banner.parentNode.removeChild(banner);
277
+ closeBtn.removeEventListener("click", closeBannerHandler);
278
+ }
279
+ closeBtn.addEventListener("click", closeBannerHandler);
280
+ }
281
+ }
282
+ }
283
+
284
+ function init(options: Partial<DefaultConfig>) {
285
+ if (!initPromise) {
286
+ mergeConfig(options);
287
+ initPromise = new Promise((resolve, reject) => {
288
+ function initializeSDK() {
289
+ fetchPricingData({
290
+ promotionId: options.promotionId,
291
+ environment: options.environment,
292
+ accessToken: options.accessToken,
293
+ })
294
+ .then(data => {
295
+ if (data) {
296
+ pricingData = data;
297
+ updatePrices();
298
+ createBanner(data);
299
+ } else {
300
+ console.error("No pricing data available");
301
+ }
302
+ resolve(pricingData);
303
+ })
304
+ .catch(error => {
305
+ console.error("Failed to fetch pricing data:", error);
306
+ reject(error);
307
+ });
308
+ }
309
+
310
+ if (document.readyState === "loading") {
311
+ document.addEventListener("DOMContentLoaded", initializeSDK);
312
+ } else {
313
+ initializeSDK();
314
+ }
315
+ });
316
+ }
317
+ return initPromise;
318
+ }
319
+
320
+ function ensureInit() {
321
+ if (!initPromise) {
322
+ return Promise.reject(
323
+ "KQPromotionUISDK.init() must be called before using other methods"
324
+ );
325
+ }
326
+ return initPromise;
327
+ }
328
+
329
+ function processQueue() {
330
+ let args;
331
+ while ((args = queue.shift())) {
332
+ const method = args.shift();
333
+ if (typeof KQPromotionUISDKPrvt[method] === "function") {
334
+ KQPromotionUISDKPrvt[method].apply(null, args);
335
+ }
336
+ }
337
+ }
338
+
339
+ const KQPromotionUISDKPrvt: KQPromotionUISDK = {
340
+ init: init,
341
+ getUpdatedPrice: function (
342
+ price: number,
343
+ options: Partial<DefaultConfig> = {}
344
+ ) {
345
+ return ensureInit().then(() => {
346
+ if (typeof price !== "number" || isNaN(price)) {
347
+ throw new Error("Invalid price provided");
348
+ }
349
+
350
+ if (!pricingData) {
351
+ throw new Error("No pricing data available");
352
+ }
353
+ const mergedOptions = {
354
+ ...config,
355
+ ...options,
356
+ };
357
+
358
+ let updatedPrice = price;
359
+
360
+ if (pricingData.percentage && !options.isOriginalDisplay) {
361
+ updatedPrice *= 1 - pricingData.percentage / 100;
362
+ }
363
+
364
+ const currencyDisplay =
365
+ (options.currencyDisplay as CurrencyDisplay) || "symbol";
366
+
367
+ const { formatted, integer, decimal, decimalSeparator } =
368
+ pdFormatCurrency({
369
+ amount: updatedPrice,
370
+ currency: mergedOptions.baseCurrencyCode,
371
+ locale: "en-US",
372
+ currencyDisplay,
373
+ formatStyle: "standard",
374
+ minimumFractionDigits: options.showDecimal
375
+ ? options.minimumDecimalDigits
376
+ : 0,
377
+ maximumFractionDigits: options.showDecimal
378
+ ? options.maximumDecimalDigits
379
+ : 0,
380
+ });
381
+
382
+ return {
383
+ price: updatedPrice,
384
+ formattedPrice: formatted,
385
+ integerPart: integer,
386
+ decimalPart: decimal || "",
387
+ decimalSeparator: decimalSeparator,
388
+ currencySymbol: mergedOptions.baseCurrencySymbol,
389
+ currencyCode: mergedOptions.baseCurrencyCode,
390
+ apiResponse: pricingData,
391
+ };
392
+ });
393
+ },
394
+ updatePriceElement: function (
395
+ element: HTMLElement,
396
+ options: Partial<DefaultConfig> = {}
397
+ ) {
398
+ return ensureInit().then(() => {
399
+ if (!element || !element.hasAttribute("data-kq-price")) {
400
+ throw new Error(
401
+ "Invalid element provided. Must have data-kq-price attribute."
402
+ );
403
+ }
404
+
405
+ const price = parseFloat(element.getAttribute("data-kq-price"));
406
+ const isOriginalDisplay = element.hasAttribute(
407
+ "data-kq-original-price-display"
408
+ );
409
+
410
+ return this.getUpdatedPrice(price, {
411
+ ...options,
412
+ isOriginalDisplay,
413
+ }).then(result => {
414
+ if (!result) return;
415
+ // Update the current element
416
+ updatePriceDisplay(element, result.price, {
417
+ ...options,
418
+ baseCurrencySymbol: result.currencySymbol,
419
+ baseCurrencyCode: result.currencyCode,
420
+ });
421
+
422
+ // Find and update related elements
423
+ const relatedElements = findRelatedElements(element);
424
+ if (relatedElements) {
425
+ relatedElements.forEach(relatedElement => {
426
+ if (relatedElement !== element) {
427
+ const relatedIsOriginalDisplay = relatedElement.hasAttribute(
428
+ "data-kq-original-price-display"
429
+ );
430
+ const relatedPrice = parseFloat(
431
+ relatedElement.getAttribute("data-kq-price") ||
432
+ relatedElement.getAttribute(
433
+ "data-kq-original-price-display"
434
+ )
435
+ );
436
+ this.getUpdatedPrice(relatedPrice, {
437
+ ...options,
438
+ isOriginalDisplay: relatedIsOriginalDisplay,
439
+ }).then(relatedResult => {
440
+ if (!relatedResult) return;
441
+ updatePriceDisplay(
442
+ relatedElement as HTMLElement,
443
+ relatedResult.price,
444
+ {
445
+ ...options,
446
+ baseCurrencySymbol: relatedResult.currencySymbol,
447
+ baseCurrencyCode: relatedResult.currencyCode,
448
+ }
449
+ );
450
+ });
451
+ }
452
+ });
453
+ }
454
+
455
+ return result;
456
+ });
457
+ });
458
+ },
459
+ updatePrice: function (
460
+ priceConfigs: {
461
+ element: HTMLElement;
462
+ price: number;
463
+ options?: Partial<DefaultConfig>;
464
+ template?: string;
465
+ }[]
466
+ ) {
467
+ return ensureInit().then(() => {
468
+ const updatePromises = priceConfigs.map(config => {
469
+ const { element, price, options, template } = config;
470
+
471
+ if (!element) {
472
+ throw new Error("Element is required for each price configuration");
473
+ }
474
+
475
+ if (typeof price !== "number" || isNaN(price)) {
476
+ throw new Error(
477
+ "Valid price is required for each price configuration"
478
+ );
479
+ }
480
+
481
+ return this.getUpdatedPrice(price, options).then(result => {
482
+ const updatedHTML = template.replace(/{{(\w+)}}/g, (match, key) => {
483
+ return result[key] || match;
484
+ });
485
+ element.innerHTML = updatedHTML;
486
+ return result;
487
+ });
488
+ });
489
+
490
+ return Promise.all(updatePromises);
491
+ });
492
+ },
493
+ };
494
+ // Replace the temporary KQPromotionUISDK function with the actual SDK
495
+ (window as unknown as any).KQPromotionUISDK = KQPromotionUISDKPrvt;
496
+
497
+ // Process any queued commands
498
+ processQueue();
499
+ };
500
+
501
+ // Only initialize in browser environment
502
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
503
+ KQPromotionUISDK(window, document);
504
+ }
package/src/types.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { CurrencyDisplay } from "./utils";
2
+
3
+ export interface Widget {
4
+ backgroundColor: string;
5
+ fontColor: string;
6
+ highlightFontColor: string;
7
+ fontSize: string;
8
+ unStyled: boolean;
9
+ addCloseIcon: boolean;
10
+ placement: string;
11
+ type: string;
12
+ borderRadius: string;
13
+ container?: string;
14
+ countryMessage?: string;
15
+ }
16
+
17
+ export interface Security {
18
+ proxy: boolean;
19
+ crawler: boolean;
20
+ vpn: boolean;
21
+ tor: boolean;
22
+ relay: boolean;
23
+ }
24
+
25
+ export interface PricingData {
26
+ country: string;
27
+ countryCode: string;
28
+ percentage: number;
29
+ code: string;
30
+ widgets: Widget[];
31
+ security: Security;
32
+ }
33
+
34
+ export interface DefaultConfig {
35
+ promotionId?: string;
36
+ showBanner: boolean;
37
+ environment: "production" | "sandbox";
38
+ localizePricing: boolean;
39
+ baseCurrencyCode: string;
40
+ baseCurrencySymbol: string;
41
+ currencyDisplay: CurrencyDisplay;
42
+ showDecimal: boolean;
43
+ minimumDecimalDigits: number;
44
+ maximumDecimalDigits: number;
45
+ isOriginalDisplay: boolean;
46
+ banner: Partial<Widget>;
47
+ pdIdentifier?: string;
48
+ accessToken?: string;
49
+ }
50
+
51
+ export interface PriceObject {
52
+ price: number;
53
+ formattedPrice: string;
54
+ integerPart: string;
55
+ decimalPart: string;
56
+ decimalSeparator: string;
57
+ currencySymbol: string;
58
+ currencyCode: string;
59
+ apiResponse: PricingData;
60
+ }