@liquidcommercedev/rmn-sdk 1.5.0-beta.10 → 1.5.0-beta.11

Sign up to get free protection for your applications and to get access to all the features.
package/dist/index.cjs CHANGED
@@ -67,7 +67,9 @@ exports.RMN_SPOT_EVENT = void 0;
67
67
  RMN_SPOT_EVENT["PURCHASE"] = "PURCHASE";
68
68
  RMN_SPOT_EVENT["ADD_TO_CART"] = "ADD_TO_CART";
69
69
  RMN_SPOT_EVENT["REMOVE_FROM_CART"] = "REMOVE_FROM_CART";
70
+ RMN_SPOT_EVENT["ADD_TO_CART_FROM_DETAILS"] = "ADD_TO_CART_FROM_DETAILS";
70
71
  RMN_SPOT_EVENT["ADD_TO_WISHLIST"] = "ADD_TO_WISHLIST";
72
+ RMN_SPOT_EVENT["EXPAND_PRODUCT"] = "EXPAND_PRODUCT";
71
73
  RMN_SPOT_EVENT["BUY_NOW"] = "BUY_NOW";
72
74
  })(exports.RMN_SPOT_EVENT || (exports.RMN_SPOT_EVENT = {}));
73
75
  exports.RMN_ENV = void 0;
@@ -6545,6 +6547,94 @@ axios.HttpStatusCode = HttpStatusCode;
6545
6547
 
6546
6548
  axios.default = axios;
6547
6549
 
6550
+ /**
6551
+ * Keyword patterns for each RMN_SPOT_EVENT.
6552
+ * Each event type has required keywords that must be present and optional ones.
6553
+ *
6554
+ * Note: The order of checks matters - more specific patterns must be checked before general ones
6555
+ * to avoid incorrect matches.
6556
+ */
6557
+ const EVENT_KEYWORDS = {
6558
+ [exports.RMN_SPOT_EVENT.ADD_TO_CART_FROM_DETAILS]: {
6559
+ required: ['add', 'cart'],
6560
+ optional: ['details', 'detail', 'single', 'profile', 'page', 'pdp'],
6561
+ },
6562
+ [exports.RMN_SPOT_EVENT.BUY_NOW]: {
6563
+ required: ['buy', 'now'],
6564
+ },
6565
+ [exports.RMN_SPOT_EVENT.EXPAND_PRODUCT]: {
6566
+ required: ['product'],
6567
+ optional: ['details', 'expand', 'modal', 'popup', 'quickview', 'view'],
6568
+ },
6569
+ [exports.RMN_SPOT_EVENT.ADD_TO_WISHLIST]: {
6570
+ required: ['add', 'wishlist'],
6571
+ },
6572
+ [exports.RMN_SPOT_EVENT.REMOVE_FROM_CART]: {
6573
+ required: ['remove', 'cart'],
6574
+ },
6575
+ [exports.RMN_SPOT_EVENT.PURCHASE]: {
6576
+ required: ['purchase'],
6577
+ },
6578
+ [exports.RMN_SPOT_EVENT.ADD_TO_CART]: {
6579
+ required: ['add', 'cart'],
6580
+ },
6581
+ };
6582
+ /**
6583
+ * Normalizes an event name by converting to lowercase, removing special characters,
6584
+ * and splitting into words.
6585
+ */
6586
+ function normalizeEventName(event) {
6587
+ return event
6588
+ .toLowerCase()
6589
+ .replace(/[^a-z0-9\s]+/g, ' ') // Replace special chars with spaces
6590
+ .split(/\s+/) // Split on whitespace
6591
+ .filter(Boolean); // Remove empty strings
6592
+ }
6593
+ /**
6594
+ * Checks if a word matches a keyword, considering word boundaries.
6595
+ * This prevents partial word matches (e.g., "card" shouldn't match "cart").
6596
+ */
6597
+ function wordMatchesKeyword(word, keyword) {
6598
+ // Create a RegExp that matches the keyword as a whole word
6599
+ const keywordRegex = new RegExp(`^${keyword}s?$|${keyword}s?\\W|\\W${keyword}s?$|\\W${keyword}s?\\W`);
6600
+ return keywordRegex.test(` ${word} `); // Add spaces to handle word boundaries
6601
+ }
6602
+ /**
6603
+ * Checks if all required keywords and at least one optional keyword (if specified) are present.
6604
+ */
6605
+ function matchesKeywordPattern(words, required, optional) {
6606
+ // Check if all required keywords are present as whole words
6607
+ const hasAllRequired = required.every((keyword) => words.some((word) => wordMatchesKeyword(word, keyword)));
6608
+ if (!hasAllRequired) {
6609
+ return false;
6610
+ }
6611
+ // If no optional keywords are specified, return true
6612
+ if (!(optional === null || optional === void 0 ? void 0 : optional.length)) {
6613
+ return true;
6614
+ }
6615
+ // If optional keywords exist, check if at least one matches as a whole word
6616
+ return optional.some((keyword) => words.some((word) => wordMatchesKeyword(word, keyword)));
6617
+ }
6618
+ /**
6619
+ * Determines the event type from a raw event string by checking for keyword patterns.
6620
+ *
6621
+ * @param {string} [event] - The raw event string to evaluate
6622
+ * @returns {RMN_SPOT_EVENT | null} - The corresponding RMN_SPOT_EVENT or null if no match
6623
+ */
6624
+ function getEventTypeFromRawEvent(event) {
6625
+ if (!(event === null || event === void 0 ? void 0 : event.trim())) {
6626
+ return null;
6627
+ }
6628
+ const words = normalizeEventName(event);
6629
+ // Use Object.entries to maintain the exact order defined in EVENT_KEYWORDS
6630
+ for (const [eventType, { required, optional }] of Object.entries(EVENT_KEYWORDS)) {
6631
+ if (matchesKeywordPattern(words, required, optional)) {
6632
+ return eventType;
6633
+ }
6634
+ }
6635
+ return null;
6636
+ }
6637
+
6548
6638
  class SingletonManager {
6549
6639
  /**
6550
6640
  * Retrieves an instance of the specified class using the provided instance creator function.
@@ -6703,12 +6793,319 @@ class ObjectHelper {
6703
6793
  *
6704
6794
  * @return {void} - This method does not return any value.
6705
6795
  */
6706
- innerMerge(target, source) {
6707
- for (const key of Object.keys(source)) {
6708
- target[key] = this.mergeValue(target[key], source[key]);
6709
- }
6796
+ innerMerge(target, source) {
6797
+ for (const key of Object.keys(source)) {
6798
+ target[key] = this.mergeValue(target[key], source[key]);
6799
+ }
6800
+ }
6801
+ }
6802
+
6803
+ /**
6804
+ * Recursively extracts ID values from a nested data structure.
6805
+ * Searches for specified property names and collects their primitive values (strings/numbers).
6806
+ *
6807
+ * @param data - The data structure to search through (can be nested objects/arrays)
6808
+ * @param propertyNames - Array of property names to look for
6809
+ * @returns Array of extracted ID values (strings/numbers only)
6810
+ *
6811
+ * @example
6812
+ * const data = {
6813
+ * id: [1, 2, 3],
6814
+ * nested: { id: 'abc' },
6815
+ * items: [{ id: 456 }]
6816
+ * };
6817
+ * extractDeepIds(data); // Returns [1, 2, 3, 'abc', 456]
6818
+ */
6819
+ function extractDeepIds(data, propertyNames) {
6820
+ const ids = [];
6821
+ const defaulPropertyNames = [
6822
+ 'id',
6823
+ 'upc',
6824
+ 'groupingId',
6825
+ 'sku',
6826
+ 'productId',
6827
+ 'item_id',
6828
+ 'isbn',
6829
+ 'asin',
6830
+ 'mpn',
6831
+ 'model_number',
6832
+ 'article_number',
6833
+ 'variant_id',
6834
+ 'item_number',
6835
+ 'catalog_id',
6836
+ 'reference_id',
6837
+ ];
6838
+ // Set for faster property name lookups
6839
+ const propertySet = new Set(defaulPropertyNames);
6840
+ /**
6841
+ * Processes a value and extracts IDs if it matches criteria
6842
+ * @param value - The value to process
6843
+ * @param currentKey - The property name of the current value
6844
+ */
6845
+ const processValue = (value, currentKey) => {
6846
+ // Early exit for null/undefined values
6847
+ if (value == null)
6848
+ return;
6849
+ // If current key matches our target properties
6850
+ if (currentKey && propertySet.has(currentKey)) {
6851
+ if (Array.isArray(value)) {
6852
+ // Filter and push valid array values in one pass
6853
+ ids.push(...value.filter((item) => typeof item === 'string' || typeof item === 'number'));
6854
+ }
6855
+ else if (typeof value === 'string' || typeof value === 'number') {
6856
+ ids.push(value);
6857
+ }
6858
+ return; // Stop processing this branch after handling the ID
6859
+ }
6860
+ // Recursively process nested structures
6861
+ if (Array.isArray(value)) {
6862
+ value.forEach((item) => processValue(item));
6863
+ }
6864
+ else if (typeof value === 'object') {
6865
+ // Process all enumerable properties
6866
+ for (const [key, val] of Object.entries(value)) {
6867
+ processValue(val, key);
6868
+ }
6869
+ }
6870
+ };
6871
+ processValue(data);
6872
+ return ids; // No need to filter nulls as we handle that during collection
6873
+ }
6874
+ // Fallback method using fetch if sendBeacon isn't available
6875
+ async function fallbackEventFire(url) {
6876
+ try {
6877
+ const racePromise = Promise.race([
6878
+ // Promise #1: The fetch request
6879
+ fetch(url, {
6880
+ method: 'POST',
6881
+ keepalive: true,
6882
+ }),
6883
+ // Promise #2: The timeout
6884
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), 2000)),
6885
+ ]);
6886
+ /**
6887
+ * Prevent requests from hanging indefinitely
6888
+ * Improve user experience by failing fast
6889
+ * Handle slow network conditions gracefully
6890
+ * Ensure resources are freed up in a timely manner
6891
+ */
6892
+ const response = await racePromise;
6893
+ return response.ok;
6894
+ }
6895
+ catch (_a) {
6896
+ return false;
6897
+ }
6898
+ }
6899
+ /**
6900
+ * Extracts and decodes a URL from a base64-encoded query parameter.
6901
+ *
6902
+ * @param {string} url - The URL containing the base64-encoded query parameter.
6903
+ * @returns {string | null} - The decoded URL or null if not found or invalid.
6904
+ */
6905
+ function getRedirectUrlFromPayload(url) {
6906
+ try {
6907
+ const base64String = new URL(url).searchParams.get('e');
6908
+ if (!base64String) {
6909
+ return null;
6910
+ }
6911
+ const data = JSON.parse(atob(base64String));
6912
+ return data.ur || null;
6913
+ }
6914
+ catch (_a) {
6915
+ return null;
6916
+ }
6917
+ }
6918
+ /**
6919
+ * Fires an event using the navigator.sendBeacon method or a fallback method if sendBeacon is not available.
6920
+ * If the event is a click event and a redirect URL is found, it redirects the user to that URL.
6921
+ *
6922
+ * @param {IFireEventParams} params - The parameters for firing the event.
6923
+ * @param {RMN_SPOT_EVENT} params.event - The event type.
6924
+ * @param {string} params.eventUrl - The URL to which the event is sent.
6925
+ * @returns {Promise<void>} - A promise that resolves when the event is fired.
6926
+ */
6927
+ async function fireEvent({ event, eventUrl }) {
6928
+ var _a;
6929
+ try {
6930
+ const didFireEvent = ((_a = navigator === null || navigator === void 0 ? void 0 : navigator.sendBeacon) === null || _a === void 0 ? void 0 : _a.call(navigator, eventUrl)) || (await fallbackEventFire(eventUrl));
6931
+ if (!didFireEvent) {
6932
+ return;
6933
+ }
6934
+ if (event === exports.RMN_SPOT_EVENT.CLICK) {
6935
+ const redirectUrl = getRedirectUrlFromPayload(eventUrl);
6936
+ if (redirectUrl) {
6937
+ window.location.href = redirectUrl;
6938
+ }
6939
+ }
6940
+ }
6941
+ catch (_b) {
6942
+ // Handle errors silently
6943
+ }
6944
+ }
6945
+ function calculateScaleFactor(elementScale) {
6946
+ // Step 1: Apply square root for non-linear scaling
6947
+ // This creates a more gradual scaling effect, especially for larger changes
6948
+ // For example:
6949
+ // - elementScale of 0.25 (1/4 size) becomes 0.5
6950
+ // - elementScale of 1 (unchanged) remains 1
6951
+ // - elementScale of 4 (4x size) becomes 2
6952
+ const baseFactor = Math.sqrt(elementScale);
6953
+ // Step 2: Apply additional dampening to further soften the scaling effect
6954
+ // The dampening factor (0.5) can be adjusted:
6955
+ // - Lower values (closer to 0) make scaling more subtle
6956
+ // - Higher values (closer to 1) make scaling more pronounced
6957
+ const dampening = 0.35;
6958
+ // Calculate the scaleFactor:
6959
+ // 1. (baseFactor - 1) represents the change from the original size
6960
+ // 2. Multiply by dampening to reduce the effect
6961
+ // 3. Add 1 to center the scaling around the original size
6962
+ // For example, if baseFactor is 2:
6963
+ // scaleFactor = 1 + (2 - 1) * 0.5 = 1.5
6964
+ const scaleFactor = 1 + (baseFactor - 1) * dampening;
6965
+ // Step 3: Define the allowed range for the scale factor
6966
+ // This ensures that the font size never changes too drastically
6967
+ const minScale = 0.35; // Font will never be smaller than 50% of original
6968
+ const maxScale = 1.5; // Font will never be larger than 150% of original
6969
+ // Step 4: Clamp the scale factor to the defined range
6970
+ // Math.min ensures the value doesn't exceed maxScale
6971
+ // Math.max ensures the value isn't less than minScale
6972
+ return Math.max(minScale, Math.min(maxScale, scaleFactor));
6973
+ }
6974
+
6975
+ class UniqueIdGenerator {
6976
+ /**
6977
+ * Initialize the generator with a node ID
6978
+ * @param nodeId Number between 0-1023 to identify this instance
6979
+ */
6980
+ static initialize(nodeId = Math.floor(Math.random() * 1024)) {
6981
+ if (nodeId < 0 || nodeId >= 1 << this.nodeBits) {
6982
+ throw new Error(`Node ID must be between 0 and ${(1 << this.nodeBits) - 1}`);
6983
+ }
6984
+ this.nodeId = nodeId;
6985
+ }
6986
+ /**
6987
+ * Convert a number to base32 string with specified length
6988
+ */
6989
+ static toBase32(num, length) {
6990
+ let result = '';
6991
+ while (num > 0) {
6992
+ result = this.base32Chars[Number(num % BigInt(32))] + result;
6993
+ // @ts-expect-error - TS doesn't support bigint division
6994
+ num = num / 32n;
6995
+ }
6996
+ return result.padStart(length, '0');
6997
+ }
6998
+ /**
6999
+ * Generate a cryptographically secure random number
7000
+ */
7001
+ static getSecureRandom() {
7002
+ if (typeof crypto !== 'undefined') {
7003
+ const buffer = new Uint32Array(1);
7004
+ crypto.getRandomValues(buffer);
7005
+ return buffer[0];
7006
+ }
7007
+ return Math.floor(Math.random() * 0xffffffff);
7008
+ }
7009
+ /**
7010
+ * Wait until next millisecond
7011
+ */
7012
+ static waitNextMillis(lastTimestamp) {
7013
+ let timestamp = Date.now();
7014
+ while (timestamp <= lastTimestamp) {
7015
+ timestamp = Date.now();
7016
+ }
7017
+ return timestamp;
7018
+ }
7019
+ /**
7020
+ * Generates a highly unique ID with the following format:
7021
+ * TTTTTTTTTTCCCCNNNNNRRRR
7022
+ * T: Timestamp (10 chars)
7023
+ * C: Counter (4 chars)
7024
+ * N: Node ID (5 chars)
7025
+ * R: Random (4 chars)
7026
+ *
7027
+ * Total length: 23 characters, always uppercase alphanumeric
7028
+ */
7029
+ static generate() {
7030
+ if (this.nodeId === undefined) {
7031
+ this.initialize();
7032
+ }
7033
+ let timestamp = Date.now() - this.epoch;
7034
+ // Handle clock moving backwards or same millisecond
7035
+ if (timestamp < this.lastTimestamp) {
7036
+ throw new Error('Clock moved backwards. Refusing to generate ID.');
7037
+ }
7038
+ if (timestamp === this.lastTimestamp) {
7039
+ this.sequence = (this.sequence + 1) & ((1 << this.sequenceBits) - 1);
7040
+ if (this.sequence === 0) {
7041
+ timestamp = this.waitNextMillis(this.lastTimestamp);
7042
+ }
7043
+ }
7044
+ else {
7045
+ this.sequence = 0;
7046
+ }
7047
+ this.lastTimestamp = timestamp;
7048
+ // Generate random component
7049
+ const random = this.getSecureRandom() & 0xffff; // 16 bits of randomness
7050
+ // Combine all components into a BigInt
7051
+ // const id =
7052
+ // (BigInt(timestamp) << BigInt(this.nodeBits + this.sequenceBits + 16)) |
7053
+ // (BigInt(this.nodeId) << BigInt(this.sequenceBits + 16)) |
7054
+ // (BigInt(this.sequence) << BigInt(16)) |
7055
+ // BigInt(random);
7056
+ // Convert to base32 representation
7057
+ const timeComponent = this.toBase32(BigInt(timestamp), 10);
7058
+ const counterComponent = this.toBase32(BigInt(this.sequence), 4);
7059
+ const nodeComponent = this.toBase32(BigInt(this.nodeId), 5);
7060
+ const randomComponent = this.toBase32(BigInt(random), 4);
7061
+ return `${timeComponent}${counterComponent}${nodeComponent}${randomComponent}`;
7062
+ }
7063
+ /**
7064
+ * Validates if a string matches the expected ID format
7065
+ */
7066
+ static isValid(id) {
7067
+ if (!/^[0-9A-HJ-NP-Z]{23}$/.test(id))
7068
+ return false;
7069
+ try {
7070
+ const timeComponent = id.slice(0, 10);
7071
+ const timestamp = this.decodeBase32(timeComponent);
7072
+ const now = Date.now() - this.epoch;
7073
+ return timestamp >= 0 && timestamp <= now;
7074
+ }
7075
+ catch (_a) {
7076
+ return false;
7077
+ }
7078
+ }
7079
+ /**
7080
+ * Decode base32 string to number
7081
+ */
7082
+ static decodeBase32(str) {
7083
+ let result = 0;
7084
+ for (const char of str) {
7085
+ result = result * 32 + this.base32Chars.indexOf(char);
7086
+ }
7087
+ return result;
7088
+ }
7089
+ /**
7090
+ * Extract timestamp from ID
7091
+ */
7092
+ static getTimestamp(id) {
7093
+ if (!this.isValid(id))
7094
+ throw new Error('Invalid ID format');
7095
+ const timeComponent = id.slice(0, 10);
7096
+ const timestamp = this.decodeBase32(timeComponent);
7097
+ return new Date(timestamp + this.epoch);
6710
7098
  }
6711
7099
  }
7100
+ // Constants for bit manipulation
7101
+ UniqueIdGenerator.epoch = 1577836800000; // 2020-01-01 as epoch
7102
+ UniqueIdGenerator.nodeBits = 10;
7103
+ UniqueIdGenerator.sequenceBits = 12;
7104
+ // Instance variables
7105
+ UniqueIdGenerator.lastTimestamp = -1;
7106
+ UniqueIdGenerator.sequence = 0;
7107
+ // Character set for base32 encoding (excluding similar looking characters)
7108
+ UniqueIdGenerator.base32Chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
6712
7109
 
6713
7110
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
6714
7111
 
@@ -15582,301 +15979,100 @@ class BaseApi extends BaseApiAbstract {
15582
15979
  let timestamp = 0;
15583
15980
  if ((response === null || response === void 0 ? void 0 : response.headers) &&
15584
15981
  ((_a = response === null || response === void 0 ? void 0 : response.headers) === null || _a === void 0 ? void 0 : _a.has) &&
15585
- ((_b = response === null || response === void 0 ? void 0 : response.headers) === null || _b === void 0 ? void 0 : _b.has(REQUEST_CLOUD_PROTECTED_KEY)) &&
15586
- ((_c = response === null || response === void 0 ? void 0 : response.headers) === null || _c === void 0 ? void 0 : _c.has(REQUEST_CLOUD_PROTECTED_TIMESTAMP)) &&
15587
- ((_d = response === null || response === void 0 ? void 0 : response.headers) === null || _d === void 0 ? void 0 : _d.get)) {
15588
- isEncrypted = ((_e = response === null || response === void 0 ? void 0 : response.headers) === null || _e === void 0 ? void 0 : _e.get(REQUEST_CLOUD_PROTECTED_KEY)) === 'true';
15589
- timestamp = ((_f = response === null || response === void 0 ? void 0 : response.headers) === null || _f === void 0 ? void 0 : _f.get(REQUEST_CLOUD_PROTECTED_TIMESTAMP))
15590
- ? Number((_g = response === null || response === void 0 ? void 0 : response.headers) === null || _g === void 0 ? void 0 : _g.get(REQUEST_CLOUD_PROTECTED_TIMESTAMP))
15591
- : 0;
15592
- }
15593
- if (responseData &&
15594
- isEncrypted &&
15595
- this.objectHelper.includes(responseData, 't') &&
15596
- timestamp > 0) {
15597
- const { t: encryptedPayload } = responseData;
15598
- const decryptedData = await this.encryptedApi.handleDecryption(encryptedPayload, timestamp);
15599
- if (decryptedData === null || decryptedData === void 0 ? void 0 : decryptedData.payload) {
15600
- delete decryptedData.payload.exp;
15601
- delete decryptedData.payload.iat;
15602
- responseData = decryptedData.payload;
15603
- }
15604
- }
15605
- return { isOk: true, val: responseData, isErr: false };
15606
- }
15607
- /**
15608
- * Creates a URL by concatenating the base URL with the provided path.
15609
- *
15610
- * @param {string | null} path - The path to be appended to the base URL.
15611
- * @return {string} The concatenated URL.
15612
- */
15613
- createURL(path) {
15614
- const formattedPath = path && path[0] !== '/' ? `/${path}` : path;
15615
- return `${this.baseUrl}/api${formattedPath}`;
15616
- }
15617
- }
15618
-
15619
- const AUTH_API_PATH = '/auth';
15620
-
15621
- class AuthService extends BaseApi {
15622
- constructor(apiKey, env) {
15623
- super({
15624
- authenticated: false,
15625
- apiKey,
15626
- token: '',
15627
- env,
15628
- });
15629
- }
15630
- /**
15631
- * Retrieves the singleton instance of the AuthService class.
15632
- *
15633
- * @param {string} apiKey - The API key used for authentication.
15634
- * @param {RMN_ENV} env - The environment enum value.
15635
- * @returns {AuthService} The singleton instance of the AuthService class.
15636
- */
15637
- static getInstance(apiKey, env) {
15638
- return SingletonManager.getInstance('AuthService', () => new AuthService(apiKey, env));
15639
- }
15640
- /**
15641
- * Initializes the authentication process.
15642
- *
15643
- * @returns {Promise<IAuthCredentials>} A Promise that resolves to the authenticated credentials.
15644
- * @throws {Error} If there is an error during authentication or the authentication response is unsuccessful.
15645
- */
15646
- async initialize() {
15647
- const { isOk, isErr, val } = await this.get(AUTH_API_PATH, {
15648
- headers: {
15649
- [SDK_CONFIG.apiHeader]: this.authInfo.apiKey,
15650
- },
15651
- });
15652
- if (isErr) {
15653
- throw new Error(`There was an error during authentication: (${isErr === null || isErr === void 0 ? void 0 : isErr.errorMessage})`);
15654
- }
15655
- if (isOk && (val === null || val === void 0 ? void 0 : val.data.token)) {
15656
- this.authInfo.token = val.data.token;
15657
- this.authInfo.authenticated = true;
15658
- }
15659
- else {
15660
- throw new Error('Auth response was not successful');
15661
- }
15662
- return this.authInfo;
15663
- }
15664
- }
15665
-
15666
- const SPOT_ELEMENT_TAG = 'spot-element';
15667
- const CAROUSEL_ELEMENT_TAG = 'spot-carousel-element';
15668
- const GFONT_PRECONNECT = `
15669
- <link rel="preconnect" href="https://fonts.googleapis.com">
15670
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15671
- `;
15672
- const GFONT_SOURCE_SANS_3 = `
15673
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap">
15674
- `;
15675
- const GFONT_CORMORANT = `
15676
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,300..700;1,300..700&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap">
15677
- `;
15678
-
15679
- /**
15680
- * Determines the event type from a raw event string.
15681
- *
15682
- * @param {string} [event] - The raw event string to evaluate.
15683
- * @returns {RMN_SPOT_EVENT | null} - The corresponding RMN_SPOT_EVENT or null if no match is found.
15684
- */
15685
- function getEventTypeFromRawEvent(event) {
15686
- if (!event) {
15687
- return null;
15688
- }
15689
- if (event.includes('cart')) {
15690
- if (event.includes('add')) {
15691
- return exports.RMN_SPOT_EVENT.ADD_TO_CART;
15692
- }
15693
- if (event.includes('remove')) {
15694
- return exports.RMN_SPOT_EVENT.REMOVE_FROM_CART;
15695
- }
15696
- }
15697
- if (event.includes('purchase')) {
15698
- return exports.RMN_SPOT_EVENT.PURCHASE;
15699
- }
15700
- // if(event.includes('refund')) {
15701
- // return RMN_SPOT_EVENT.REFUND;
15702
- // }
15703
- if (event.includes('wishlist') && event.includes('add')) {
15704
- return exports.RMN_SPOT_EVENT.ADD_TO_WISHLIST;
15705
- }
15706
- return null;
15707
- }
15708
- /**
15709
- * Recursively extracts ID values from a nested data structure.
15710
- * Searches for specified property names and collects their primitive values (strings/numbers).
15711
- *
15712
- * @param data - The data structure to search through (can be nested objects/arrays)
15713
- * @param propertyNames - Array of property names to look for
15714
- * @returns Array of extracted ID values (strings/numbers only)
15715
- *
15716
- * @example
15717
- * const data = {
15718
- * id: [1, 2, 3],
15719
- * nested: { id: 'abc' },
15720
- * items: [{ id: 456 }]
15721
- * };
15722
- * extractDeepIds(data); // Returns [1, 2, 3, 'abc', 456]
15723
- */
15724
- function extractDeepIds(data, propertyNames) {
15725
- const ids = [];
15726
- const defaulPropertyNames = [
15727
- 'id',
15728
- 'upc',
15729
- 'groupingId',
15730
- 'sku',
15731
- 'productId',
15732
- 'item_id',
15733
- 'isbn',
15734
- 'asin',
15735
- 'mpn',
15736
- 'model_number',
15737
- 'article_number',
15738
- 'variant_id',
15739
- 'item_number',
15740
- 'catalog_id',
15741
- 'reference_id',
15742
- ];
15743
- // Set for faster property name lookups
15744
- const propertySet = new Set(defaulPropertyNames);
15745
- /**
15746
- * Processes a value and extracts IDs if it matches criteria
15747
- * @param value - The value to process
15748
- * @param currentKey - The property name of the current value
15749
- */
15750
- const processValue = (value, currentKey) => {
15751
- // Early exit for null/undefined values
15752
- if (value == null)
15753
- return;
15754
- // If current key matches our target properties
15755
- if (currentKey && propertySet.has(currentKey)) {
15756
- if (Array.isArray(value)) {
15757
- // Filter and push valid array values in one pass
15758
- ids.push(...value.filter((item) => typeof item === 'string' || typeof item === 'number'));
15759
- }
15760
- else if (typeof value === 'string' || typeof value === 'number') {
15761
- ids.push(value);
15762
- }
15763
- return; // Stop processing this branch after handling the ID
15764
- }
15765
- // Recursively process nested structures
15766
- if (Array.isArray(value)) {
15767
- value.forEach((item) => processValue(item));
15768
- }
15769
- else if (typeof value === 'object') {
15770
- // Process all enumerable properties
15771
- for (const [key, val] of Object.entries(value)) {
15772
- processValue(val, key);
15773
- }
15774
- }
15775
- };
15776
- processValue(data);
15777
- return ids; // No need to filter nulls as we handle that during collection
15778
- }
15779
- // Fallback method using fetch if sendBeacon isn't available
15780
- async function fallbackEventFire(url) {
15781
- try {
15782
- const racePromise = Promise.race([
15783
- // Promise #1: The fetch request
15784
- fetch(url, {
15785
- method: 'POST',
15786
- keepalive: true,
15787
- }),
15788
- // Promise #2: The timeout
15789
- new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), 2000)),
15790
- ]);
15791
- /**
15792
- * Prevent requests from hanging indefinitely
15793
- * Improve user experience by failing fast
15794
- * Handle slow network conditions gracefully
15795
- * Ensure resources are freed up in a timely manner
15796
- */
15797
- const response = await racePromise;
15798
- return response.ok;
15799
- }
15800
- catch (_a) {
15801
- return false;
15802
- }
15803
- }
15804
- /**
15805
- * Extracts and decodes a URL from a base64-encoded query parameter.
15806
- *
15807
- * @param {string} url - The URL containing the base64-encoded query parameter.
15808
- * @returns {string | null} - The decoded URL or null if not found or invalid.
15809
- */
15810
- function getRedirectUrlFromPayload(url) {
15811
- try {
15812
- const base64String = new URL(url).searchParams.get('e');
15813
- if (!base64String) {
15814
- return null;
15815
- }
15816
- const data = JSON.parse(atob(base64String));
15817
- return data.ur || null;
15818
- }
15819
- catch (_a) {
15820
- return null;
15821
- }
15822
- }
15823
- /**
15824
- * Fires an event using the navigator.sendBeacon method or a fallback method if sendBeacon is not available.
15825
- * If the event is a click event and a redirect URL is found, it redirects the user to that URL.
15826
- *
15827
- * @param {IFireEventParams} params - The parameters for firing the event.
15828
- * @param {RMN_SPOT_EVENT} params.event - The event type.
15829
- * @param {string} params.eventUrl - The URL to which the event is sent.
15830
- * @returns {Promise<void>} - A promise that resolves when the event is fired.
15831
- */
15832
- async function fireEvent({ event, eventUrl }) {
15833
- var _a;
15834
- try {
15835
- const didFireEvent = ((_a = navigator === null || navigator === void 0 ? void 0 : navigator.sendBeacon) === null || _a === void 0 ? void 0 : _a.call(navigator, eventUrl)) || (await fallbackEventFire(eventUrl));
15836
- if (!didFireEvent) {
15837
- return;
15982
+ ((_b = response === null || response === void 0 ? void 0 : response.headers) === null || _b === void 0 ? void 0 : _b.has(REQUEST_CLOUD_PROTECTED_KEY)) &&
15983
+ ((_c = response === null || response === void 0 ? void 0 : response.headers) === null || _c === void 0 ? void 0 : _c.has(REQUEST_CLOUD_PROTECTED_TIMESTAMP)) &&
15984
+ ((_d = response === null || response === void 0 ? void 0 : response.headers) === null || _d === void 0 ? void 0 : _d.get)) {
15985
+ isEncrypted = ((_e = response === null || response === void 0 ? void 0 : response.headers) === null || _e === void 0 ? void 0 : _e.get(REQUEST_CLOUD_PROTECTED_KEY)) === 'true';
15986
+ timestamp = ((_f = response === null || response === void 0 ? void 0 : response.headers) === null || _f === void 0 ? void 0 : _f.get(REQUEST_CLOUD_PROTECTED_TIMESTAMP))
15987
+ ? Number((_g = response === null || response === void 0 ? void 0 : response.headers) === null || _g === void 0 ? void 0 : _g.get(REQUEST_CLOUD_PROTECTED_TIMESTAMP))
15988
+ : 0;
15838
15989
  }
15839
- if (event === exports.RMN_SPOT_EVENT.CLICK) {
15840
- const redirectUrl = getRedirectUrlFromPayload(eventUrl);
15841
- if (redirectUrl) {
15842
- window.location.href = redirectUrl;
15990
+ if (responseData &&
15991
+ isEncrypted &&
15992
+ this.objectHelper.includes(responseData, 't') &&
15993
+ timestamp > 0) {
15994
+ const { t: encryptedPayload } = responseData;
15995
+ const decryptedData = await this.encryptedApi.handleDecryption(encryptedPayload, timestamp);
15996
+ if (decryptedData === null || decryptedData === void 0 ? void 0 : decryptedData.payload) {
15997
+ delete decryptedData.payload.exp;
15998
+ delete decryptedData.payload.iat;
15999
+ responseData = decryptedData.payload;
15843
16000
  }
15844
16001
  }
16002
+ return { isOk: true, val: responseData, isErr: false };
15845
16003
  }
15846
- catch (_b) {
15847
- // Handle errors silently
16004
+ /**
16005
+ * Creates a URL by concatenating the base URL with the provided path.
16006
+ *
16007
+ * @param {string | null} path - The path to be appended to the base URL.
16008
+ * @return {string} The concatenated URL.
16009
+ */
16010
+ createURL(path) {
16011
+ const formattedPath = path && path[0] !== '/' ? `/${path}` : path;
16012
+ return `${this.baseUrl}/api${formattedPath}`;
15848
16013
  }
15849
16014
  }
15850
- function calculateScaleFactor(elementScale) {
15851
- // Step 1: Apply square root for non-linear scaling
15852
- // This creates a more gradual scaling effect, especially for larger changes
15853
- // For example:
15854
- // - elementScale of 0.25 (1/4 size) becomes 0.5
15855
- // - elementScale of 1 (unchanged) remains 1
15856
- // - elementScale of 4 (4x size) becomes 2
15857
- const baseFactor = Math.sqrt(elementScale);
15858
- // Step 2: Apply additional dampening to further soften the scaling effect
15859
- // The dampening factor (0.5) can be adjusted:
15860
- // - Lower values (closer to 0) make scaling more subtle
15861
- // - Higher values (closer to 1) make scaling more pronounced
15862
- const dampening = 0.35;
15863
- // Calculate the scaleFactor:
15864
- // 1. (baseFactor - 1) represents the change from the original size
15865
- // 2. Multiply by dampening to reduce the effect
15866
- // 3. Add 1 to center the scaling around the original size
15867
- // For example, if baseFactor is 2:
15868
- // scaleFactor = 1 + (2 - 1) * 0.5 = 1.5
15869
- const scaleFactor = 1 + (baseFactor - 1) * dampening;
15870
- // Step 3: Define the allowed range for the scale factor
15871
- // This ensures that the font size never changes too drastically
15872
- const minScale = 0.35; // Font will never be smaller than 50% of original
15873
- const maxScale = 1.5; // Font will never be larger than 150% of original
15874
- // Step 4: Clamp the scale factor to the defined range
15875
- // Math.min ensures the value doesn't exceed maxScale
15876
- // Math.max ensures the value isn't less than minScale
15877
- return Math.max(minScale, Math.min(maxScale, scaleFactor));
16015
+
16016
+ const AUTH_API_PATH = '/auth';
16017
+
16018
+ class AuthService extends BaseApi {
16019
+ constructor(apiKey, env) {
16020
+ super({
16021
+ authenticated: false,
16022
+ apiKey,
16023
+ token: '',
16024
+ env,
16025
+ });
16026
+ }
16027
+ /**
16028
+ * Retrieves the singleton instance of the AuthService class.
16029
+ *
16030
+ * @param {string} apiKey - The API key used for authentication.
16031
+ * @param {RMN_ENV} env - The environment enum value.
16032
+ * @returns {AuthService} The singleton instance of the AuthService class.
16033
+ */
16034
+ static getInstance(apiKey, env) {
16035
+ return SingletonManager.getInstance('AuthService', () => new AuthService(apiKey, env));
16036
+ }
16037
+ /**
16038
+ * Initializes the authentication process.
16039
+ *
16040
+ * @returns {Promise<IAuthCredentials>} A Promise that resolves to the authenticated credentials.
16041
+ * @throws {Error} If there is an error during authentication or the authentication response is unsuccessful.
16042
+ */
16043
+ async initialize() {
16044
+ const { isOk, isErr, val } = await this.get(AUTH_API_PATH, {
16045
+ headers: {
16046
+ [SDK_CONFIG.apiHeader]: this.authInfo.apiKey,
16047
+ },
16048
+ });
16049
+ if (isErr) {
16050
+ throw new Error(`There was an error during authentication: (${isErr === null || isErr === void 0 ? void 0 : isErr.errorMessage})`);
16051
+ }
16052
+ if (isOk && (val === null || val === void 0 ? void 0 : val.data.token)) {
16053
+ this.authInfo.token = val.data.token;
16054
+ this.authInfo.authenticated = true;
16055
+ }
16056
+ else {
16057
+ throw new Error('Auth response was not successful');
16058
+ }
16059
+ return this.authInfo;
16060
+ }
15878
16061
  }
15879
16062
 
16063
+ const SPOT_ELEMENT_TAG = 'spot-element';
16064
+ const CAROUSEL_ELEMENT_TAG = 'spot-carousel-element';
16065
+ const GFONT_PRECONNECT = `
16066
+ <link rel="preconnect" href="https://fonts.googleapis.com">
16067
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16068
+ `;
16069
+ const GFONT_SOURCE_SANS_3 = `
16070
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap">
16071
+ `;
16072
+ const GFONT_CORMORANT = `
16073
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,300..700;1,300..700&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap">
16074
+ `;
16075
+
15880
16076
  class IntersectionObserverService {
15881
16077
  constructor(defaultOptions = {}) {
15882
16078
  this.observers = new Map();
@@ -16867,141 +17063,6 @@ class ElementService {
16867
17063
  }
16868
17064
  }
16869
17065
 
16870
- class UniqueIdGenerator {
16871
- /**
16872
- * Initialize the generator with a node ID
16873
- * @param nodeId Number between 0-1023 to identify this instance
16874
- */
16875
- static initialize(nodeId = Math.floor(Math.random() * 1024)) {
16876
- if (nodeId < 0 || nodeId >= 1 << this.nodeBits) {
16877
- throw new Error(`Node ID must be between 0 and ${(1 << this.nodeBits) - 1}`);
16878
- }
16879
- this.nodeId = nodeId;
16880
- }
16881
- /**
16882
- * Convert a number to base32 string with specified length
16883
- */
16884
- static toBase32(num, length) {
16885
- let result = '';
16886
- while (num > 0) {
16887
- result = this.base32Chars[Number(num % BigInt(32))] + result;
16888
- // @ts-expect-error - TS doesn't support bigint division
16889
- num = num / 32n;
16890
- }
16891
- return result.padStart(length, '0');
16892
- }
16893
- /**
16894
- * Generate a cryptographically secure random number
16895
- */
16896
- static getSecureRandom() {
16897
- if (typeof crypto !== 'undefined') {
16898
- const buffer = new Uint32Array(1);
16899
- crypto.getRandomValues(buffer);
16900
- return buffer[0];
16901
- }
16902
- return Math.floor(Math.random() * 0xffffffff);
16903
- }
16904
- /**
16905
- * Wait until next millisecond
16906
- */
16907
- static waitNextMillis(lastTimestamp) {
16908
- let timestamp = Date.now();
16909
- while (timestamp <= lastTimestamp) {
16910
- timestamp = Date.now();
16911
- }
16912
- return timestamp;
16913
- }
16914
- /**
16915
- * Generates a highly unique ID with the following format:
16916
- * TTTTTTTTTTCCCCNNNNNRRRR
16917
- * T: Timestamp (10 chars)
16918
- * C: Counter (4 chars)
16919
- * N: Node ID (5 chars)
16920
- * R: Random (4 chars)
16921
- *
16922
- * Total length: 23 characters, always uppercase alphanumeric
16923
- */
16924
- static generate() {
16925
- if (this.nodeId === undefined) {
16926
- this.initialize();
16927
- }
16928
- let timestamp = Date.now() - this.epoch;
16929
- // Handle clock moving backwards or same millisecond
16930
- if (timestamp < this.lastTimestamp) {
16931
- throw new Error('Clock moved backwards. Refusing to generate ID.');
16932
- }
16933
- if (timestamp === this.lastTimestamp) {
16934
- this.sequence = (this.sequence + 1) & ((1 << this.sequenceBits) - 1);
16935
- if (this.sequence === 0) {
16936
- timestamp = this.waitNextMillis(this.lastTimestamp);
16937
- }
16938
- }
16939
- else {
16940
- this.sequence = 0;
16941
- }
16942
- this.lastTimestamp = timestamp;
16943
- // Generate random component
16944
- const random = this.getSecureRandom() & 0xffff; // 16 bits of randomness
16945
- // Combine all components into a BigInt
16946
- // const id =
16947
- // (BigInt(timestamp) << BigInt(this.nodeBits + this.sequenceBits + 16)) |
16948
- // (BigInt(this.nodeId) << BigInt(this.sequenceBits + 16)) |
16949
- // (BigInt(this.sequence) << BigInt(16)) |
16950
- // BigInt(random);
16951
- // Convert to base32 representation
16952
- const timeComponent = this.toBase32(BigInt(timestamp), 10);
16953
- const counterComponent = this.toBase32(BigInt(this.sequence), 4);
16954
- const nodeComponent = this.toBase32(BigInt(this.nodeId), 5);
16955
- const randomComponent = this.toBase32(BigInt(random), 4);
16956
- return `${timeComponent}${counterComponent}${nodeComponent}${randomComponent}`;
16957
- }
16958
- /**
16959
- * Validates if a string matches the expected ID format
16960
- */
16961
- static isValid(id) {
16962
- if (!/^[0-9A-HJ-NP-Z]{23}$/.test(id))
16963
- return false;
16964
- try {
16965
- const timeComponent = id.slice(0, 10);
16966
- const timestamp = this.decodeBase32(timeComponent);
16967
- const now = Date.now() - this.epoch;
16968
- return timestamp >= 0 && timestamp <= now;
16969
- }
16970
- catch (_a) {
16971
- return false;
16972
- }
16973
- }
16974
- /**
16975
- * Decode base32 string to number
16976
- */
16977
- static decodeBase32(str) {
16978
- let result = 0;
16979
- for (const char of str) {
16980
- result = result * 32 + this.base32Chars.indexOf(char);
16981
- }
16982
- return result;
16983
- }
16984
- /**
16985
- * Extract timestamp from ID
16986
- */
16987
- static getTimestamp(id) {
16988
- if (!this.isValid(id))
16989
- throw new Error('Invalid ID format');
16990
- const timeComponent = id.slice(0, 10);
16991
- const timestamp = this.decodeBase32(timeComponent);
16992
- return new Date(timestamp + this.epoch);
16993
- }
16994
- }
16995
- // Constants for bit manipulation
16996
- UniqueIdGenerator.epoch = 1577836800000; // 2020-01-01 as epoch
16997
- UniqueIdGenerator.nodeBits = 10;
16998
- UniqueIdGenerator.sequenceBits = 12;
16999
- // Instance variables
17000
- UniqueIdGenerator.lastTimestamp = -1;
17001
- UniqueIdGenerator.sequence = 0;
17002
- // Character set for base32 encoding (excluding similar looking characters)
17003
- UniqueIdGenerator.base32Chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
17004
-
17005
17066
  function convertHexToRgba(hex, opacity = 1) {
17006
17067
  // Remove # if present
17007
17068
  const cleanHex = hex.replace('#', '');
@@ -18884,7 +18945,7 @@ class MonitorService {
18884
18945
  this.implementedMonitor.start();
18885
18946
  }
18886
18947
  async matchAndFireEvent(eventData, spots) {
18887
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
18948
+ var _a, _b;
18888
18949
  if (!spots)
18889
18950
  return;
18890
18951
  const eventProductIds = new Set(eventData.productIds);
@@ -18893,47 +18954,13 @@ class MonitorService {
18893
18954
  continue;
18894
18955
  const hasCommonProductIds = spot.productIds.find((productId) => eventProductIds.has(productId));
18895
18956
  if (hasCommonProductIds) {
18896
- switch (eventData.event) {
18897
- case exports.RMN_SPOT_EVENT.ADD_TO_CART:
18898
- await this.fireAndPublishSpotEvent({
18899
- spotEvent: exports.RMN_SPOT_EVENT.ADD_TO_CART,
18900
- eventUrl: (_b = (_a = spot.events.find((event) => event.event === exports.RMN_SPOT_EVENT.ADD_TO_CART)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : '',
18901
- placementId: spot.placementId,
18902
- spotId: spot.spotId,
18903
- });
18904
- break;
18905
- case exports.RMN_SPOT_EVENT.REMOVE_FROM_CART:
18906
- await this.fireAndPublishSpotEvent({
18907
- spotEvent: exports.RMN_SPOT_EVENT.REMOVE_FROM_CART,
18908
- eventUrl: (_d = (_c = spot.events.find((event) => event.event === exports.RMN_SPOT_EVENT.REMOVE_FROM_CART)) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : '',
18909
- placementId: spot.placementId,
18910
- spotId: spot.spotId,
18911
- });
18912
- break;
18913
- case exports.RMN_SPOT_EVENT.PURCHASE:
18914
- await this.fireAndPublishSpotEvent({
18915
- spotEvent: exports.RMN_SPOT_EVENT.PURCHASE,
18916
- eventUrl: (_f = (_e = spot.events.find((event) => event.event === exports.RMN_SPOT_EVENT.PURCHASE)) === null || _e === void 0 ? void 0 : _e.url) !== null && _f !== void 0 ? _f : '',
18917
- placementId: spot.placementId,
18918
- spotId: spot.spotId,
18919
- });
18920
- break;
18921
- case exports.RMN_SPOT_EVENT.ADD_TO_WISHLIST:
18922
- await this.fireAndPublishSpotEvent({
18923
- spotEvent: exports.RMN_SPOT_EVENT.ADD_TO_WISHLIST,
18924
- eventUrl: (_h = (_g = spot.events.find((event) => event.event === exports.RMN_SPOT_EVENT.ADD_TO_WISHLIST)) === null || _g === void 0 ? void 0 : _g.url) !== null && _h !== void 0 ? _h : '',
18925
- placementId: spot.placementId,
18926
- spotId: spot.spotId,
18927
- });
18928
- break;
18929
- case exports.RMN_SPOT_EVENT.BUY_NOW:
18930
- await this.fireAndPublishSpotEvent({
18931
- spotEvent: exports.RMN_SPOT_EVENT.BUY_NOW,
18932
- eventUrl: (_k = (_j = spot.events.find((event) => event.event === exports.RMN_SPOT_EVENT.BUY_NOW)) === null || _j === void 0 ? void 0 : _j.url) !== null && _k !== void 0 ? _k : '',
18933
- placementId: spot.placementId,
18934
- spotId: spot.spotId,
18935
- });
18936
- break;
18957
+ if (Object.values(exports.RMN_SPOT_EVENT).includes(eventData.event)) {
18958
+ await this.fireAndPublishSpotEvent({
18959
+ spotEvent: eventData.event,
18960
+ eventUrl: (_b = (_a = spot.events.find((event) => event.event === eventData.event)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : '',
18961
+ placementId: spot.placementId,
18962
+ spotId: spot.spotId,
18963
+ });
18937
18964
  }
18938
18965
  }
18939
18966
  }
@@ -19111,7 +19138,7 @@ class EventService {
19111
19138
  spotId: spot.id,
19112
19139
  spotType: spot.spot,
19113
19140
  events: spot.events,
19114
- productIds: (_c = spot.productIds) !== null && _c !== void 0 ? _c : [],
19141
+ productIds: (_c = spot.productIds) !== null && _c !== void 0 ? _c : [1, 2, 3],
19115
19142
  });
19116
19143
  }
19117
19144
  handleIntersectionObserver(placementId, _spot, spotElement) {