@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.
- package/.env.development +3 -0
- package/.env.production +2 -0
- package/.eslintignore +6 -0
- package/.eslintrc +18 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +1 -0
- package/.lintstagedrc +4 -0
- package/.prettierignore +6 -0
- package/.prettierrc +15 -0
- package/.stylelintignore +6 -0
- package/.stylelintrc +20 -0
- package/CHANGELOG.md +24 -0
- package/DEVELOPER.md +61 -0
- package/LICENSE.md +9 -0
- package/README.md +1 -0
- package/commitlint.config.cjs +4 -0
- package/dts-bundle-generator.config.ts +11 -0
- package/favicon.svg +15 -0
- package/index.html +485 -0
- package/package.json +79 -0
- package/scripts/publish.sh +67 -0
- package/src/dom-utils.ts +6 -0
- package/src/global.d.ts +9 -0
- package/src/http-utils.ts +62 -0
- package/src/index.ts +2 -0
- package/src/public-script.js +44 -0
- package/src/sdk.ts +504 -0
- package/src/types.ts +60 -0
- package/src/utils.ts +146 -0
- package/src/vite-env.d.ts +1 -0
- package/test/dom-utils.test.ts +49 -0
- package/test/http-utils.test.ts +152 -0
- package/test/sdk.test.ts +575 -0
- package/test/setup.ts +83 -0
- package/test/utils.test.ts +137 -0
- package/tsconfig.json +26 -0
- package/vite.config.ts +57 -0
|
@@ -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,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
|
+
}
|