@cshah18/sdk 4.8.0 → 4.10.0

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.
@@ -1,23 +1,373 @@
1
+ /**
2
+ * Base class for all CoBuy SDK errors
3
+ * Provides consistent error structure across the SDK
4
+ */
5
+ class CoBuyError extends Error {
6
+ constructor(message, code, details) {
7
+ super(message);
8
+ this.name = this.constructor.name;
9
+ this.code = code;
10
+ this.details = details;
11
+ Object.setPrototypeOf(this, new.target.prototype);
12
+ }
13
+ }
1
14
  /**
2
15
  * Thrown when the SDK is used before initialization
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * try {
20
+ * CoBuy.renderWidget({...});
21
+ * } catch (error) {
22
+ * if (error instanceof CoBuyNotInitializedError) {
23
+ * console.log('SDK not initialized, call CoBuy.init() first');
24
+ * }
25
+ * }
26
+ * ```
3
27
  */
4
- class CoBuyNotInitializedError extends Error {
28
+ class CoBuyNotInitializedError extends CoBuyError {
5
29
  constructor() {
6
- super("CoBuy SDK has not been initialized. Call CoBuy.init(options) first.");
7
- this.name = "CoBuyNotInitializedError";
30
+ super("CoBuy SDK is not initialized. Call CoBuy.init() before using the SDK.", "NOT_INITIALIZED");
8
31
  Object.setPrototypeOf(this, CoBuyNotInitializedError.prototype);
9
32
  }
10
33
  }
11
34
  /**
12
- * Thrown when invalid configuration is provided
35
+ * Thrown when invalid configuration is provided during initialization
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * try {
40
+ * CoBuy.init({
41
+ * authMode: 'public'
42
+ * // Missing merchantKey
43
+ * });
44
+ * } catch (error) {
45
+ * if (error instanceof CoBuyInvalidConfigError) {
46
+ * console.log('Config validation failed:', error.message);
47
+ * }
48
+ * }
49
+ * ```
13
50
  */
14
- class CoBuyInvalidConfigError extends Error {
15
- constructor(message) {
16
- super(`Invalid CoBuy configuration: ${message}`);
17
- this.name = "CoBuyInvalidConfigError";
51
+ class CoBuyInvalidConfigError extends CoBuyError {
52
+ constructor(message, details) {
53
+ super(`Invalid CoBuy configuration: ${message}`, "INVALID_CONFIG", details);
18
54
  Object.setPrototypeOf(this, CoBuyInvalidConfigError.prototype);
19
55
  }
20
56
  }
57
+ /**
58
+ * Thrown when widget rendering fails
59
+ * This includes container resolution, DOM manipulation, or state errors
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * try {
64
+ * await CoBuy.renderWidget({
65
+ * productId: 'prod_123',
66
+ * container: '#nonexistent'
67
+ * });
68
+ * } catch (error) {
69
+ * if (error instanceof CoBuyRenderError) {
70
+ * console.log('Widget render failed:', error.message);
71
+ * }
72
+ * }
73
+ * ```
74
+ */
75
+ class CoBuyRenderError extends CoBuyError {
76
+ constructor(message, details) {
77
+ super(message, "RENDER_FAILED", details);
78
+ Object.setPrototypeOf(this, CoBuyRenderError.prototype);
79
+ }
80
+ }
81
+ /**
82
+ * Thrown when API communication fails
83
+ * This includes network errors, CORS issues, timeouts, and server errors
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * try {
88
+ * await CoBuy.joinGroup('grp_123', {type: 'email', value: 'user@example.com'});
89
+ * } catch (error) {
90
+ * if (error instanceof CoBuyApiError) {
91
+ * console.log('API request failed:', error.code, error.message);
92
+ * if (error.details?.statusCode === 429) {
93
+ * console.log('Rate limited - retry later');
94
+ * }
95
+ * }
96
+ * }
97
+ * ```
98
+ */
99
+ class CoBuyApiError extends CoBuyError {
100
+ constructor(message, code = "API_ERROR", details) {
101
+ super(message, code, details);
102
+ Object.setPrototypeOf(this, CoBuyApiError.prototype);
103
+ }
104
+ }
105
+ /**
106
+ * Thrown when input validation fails
107
+ * This includes invalid product IDs, container selectors, or option values
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * try {
112
+ * await CoBuy.renderWidget({
113
+ * productId: '', // Invalid: empty string
114
+ * container: '#widget'
115
+ * });
116
+ * } catch (error) {
117
+ * if (error instanceof CoBuyValidationError) {
118
+ * console.log('Invalid input:', error.message);
119
+ * }
120
+ * }
121
+ * ```
122
+ */
123
+ class CoBuyValidationError extends CoBuyError {
124
+ constructor(message, details) {
125
+ super(message, "VALIDATION_FAILED", details);
126
+ Object.setPrototypeOf(this, CoBuyValidationError.prototype);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Input validation helpers for SDK configuration and widget options
132
+ */
133
+ /**
134
+ * Validate product ID format and constraints
135
+ *
136
+ * @param {string} productId - The product ID to validate
137
+ * @returns {boolean} True if product ID is valid, false otherwise
138
+ *
139
+ * @description
140
+ * Validates that a product ID meets the following criteria:
141
+ * - Must be a non-empty string
142
+ * - Maximum length of 200 characters
143
+ * - Cannot be only whitespace
144
+ *
145
+ * @example
146
+ * validateProductId("prod_123"); // ✅ true
147
+ * validateProductId(""); // ❌ false (empty)
148
+ * validateProductId("x".repeat(201)); // ❌ false (too long)
149
+ */
150
+ function validateProductId(productId) {
151
+ if (!productId || typeof productId !== "string") {
152
+ return false;
153
+ }
154
+ // Prevent extremely long IDs
155
+ if (productId.length > 200) {
156
+ return false;
157
+ }
158
+ // Trim and check again
159
+ if (productId.trim().length === 0) {
160
+ return false;
161
+ }
162
+ return true;
163
+ }
164
+ /**
165
+ * Validate CSS color format
166
+ *
167
+ * @param {string} color - The color value to validate
168
+ * @returns {boolean} True if color is valid CSS format, false otherwise
169
+ *
170
+ * @description
171
+ * Validates that a color value is valid CSS format. Supports:
172
+ * - **Hex Colors**: #RGB, #RRGGBB, #RRGGBBAA
173
+ * - **RGB/RGBA**: rgb(255, 0, 0), rgba(255, 0, 0, 0.5)
174
+ * - **HSL/HSLA**: hsl(0, 100%, 50%), hsla(0, 100%, 50%, 0.5)
175
+ * - **Named Colors**: transparent, inherit, initial, unset
176
+ *
177
+ * @example
178
+ * validateColor("#4f46e5"); // ✅ true (hex)
179
+ * validateColor("#4f46e5cc"); // ✅ true (hex with alpha)
180
+ * validateColor("rgb(0, 0, 0)"); // ✅ true (rgb)
181
+ * validateColor("notacolor"); // ❌ false (invalid)
182
+ */
183
+ function validateColor(color) {
184
+ if (!color || typeof color !== "string") {
185
+ return false;
186
+ }
187
+ const trimmedColor = color.trim();
188
+ // Hex color validation: #RGB, #RRGGBB, #RRGGBBAA
189
+ if (/^#([0-9A-F]{3}){1,2}([0-9A-F]{2})?$/i.test(trimmedColor)) {
190
+ return true;
191
+ }
192
+ // RGB/RGBA validation
193
+ if (/^rgba?\s*\(/i.test(trimmedColor)) {
194
+ return true;
195
+ }
196
+ // HSL/HSLA validation
197
+ if (/^hsla?\s*\(/i.test(trimmedColor)) {
198
+ return true;
199
+ }
200
+ // Named colors
201
+ const namedColors = ["transparent", "inherit", "initial", "unset"];
202
+ if (namedColors.includes(trimmedColor.toLowerCase())) {
203
+ return true;
204
+ }
205
+ return false;
206
+ }
207
+ /**
208
+ * Validate CSS border radius format
209
+ *
210
+ * @param {string} borderRadius - The border radius value to validate
211
+ * @returns {boolean} True if border radius is valid CSS format, false otherwise
212
+ *
213
+ * @description
214
+ * Validates that a border radius value is valid CSS format.
215
+ * Supports common CSS units: px, rem, em, %, ch, ex, vw, vh, vmin, vmax
216
+ *
217
+ * @example
218
+ * validateBorderRadius("8px"); // ✅ true
219
+ * validateBorderRadius("0.5rem"); // ✅ true
220
+ * validateBorderRadius("8"); // ✅ true (assumes pixels)
221
+ * validateBorderRadius("notsize"); // ❌ false
222
+ */
223
+ function validateBorderRadius(borderRadius) {
224
+ if (!borderRadius || typeof borderRadius !== "string") {
225
+ return false;
226
+ }
227
+ const trimmedRadius = borderRadius.trim();
228
+ // Valid CSS size units
229
+ if (/^(\d+(\.\d+)?)(px|rem|em|%|ch|ex|vw|vh|vmin|vmax)$/i.test(trimmedRadius)) {
230
+ return true;
231
+ }
232
+ // Single number (assumes pixels)
233
+ if (/^\d+(\.\d+)?$/.test(trimmedRadius)) {
234
+ return true;
235
+ }
236
+ return false;
237
+ }
238
+ /**
239
+ * Validate CSS font family format
240
+ *
241
+ * @param {string} fontFamily - The font family value to validate
242
+ * @returns {boolean} True if font family is valid, false otherwise
243
+ *
244
+ * @description
245
+ * Validates that a font family value is suitable for use.
246
+ * - Must not be empty or whitespace-only
247
+ * - Maximum length of 500 characters
248
+ * - Can be single font or comma-separated list
249
+ *
250
+ * @example
251
+ * validateFontFamily("Inter, sans-serif"); // ✅ true
252
+ * validateFontFamily("'Courier New', monospace"); // ✅ true
253
+ * validateFontFamily(""); // ❌ false (empty)
254
+ */
255
+ function validateFontFamily(fontFamily) {
256
+ if (!fontFamily || typeof fontFamily !== "string") {
257
+ return false;
258
+ }
259
+ const trimmedFamily = fontFamily.trim();
260
+ // Ensure non-empty after trim
261
+ if (trimmedFamily.length === 0) {
262
+ return false;
263
+ }
264
+ // Max length check to prevent extremely long font lists
265
+ if (trimmedFamily.length > 500) {
266
+ return false;
267
+ }
268
+ return true;
269
+ }
270
+ /**
271
+ * Validate animation speed value
272
+ *
273
+ * @param {string} speed - The animation speed to validate
274
+ * @returns {boolean} True if speed is a valid animation speed value, false otherwise
275
+ *
276
+ * @description
277
+ * Validates that the animation speed is one of the predefined values.
278
+ * Valid values: "none", "fast", "normal", "slow"
279
+ *
280
+ * @example
281
+ * validateAnimationSpeed("normal"); // ✅ true
282
+ * validateAnimationSpeed("slow"); // ✅ true
283
+ * validateAnimationSpeed("fast"); // ✅ true (case-insensitive)
284
+ * validateAnimationSpeed("SLOW"); // ✅ true
285
+ * validateAnimationSpeed("instant"); // ❌ false (invalid value)
286
+ */
287
+ function validateAnimationSpeed(speed) {
288
+ const validSpeeds = ["none", "fast", "normal", "slow"];
289
+ return validSpeeds.includes(speed.toLowerCase());
290
+ }
291
+ /**
292
+ * Validate animation style value
293
+ *
294
+ * @param {string} style - The animation style to validate
295
+ * @returns {boolean} True if style is a valid animation style value, false otherwise
296
+ *
297
+ * @description
298
+ * Validates that the animation style is one of the predefined values.
299
+ * Valid values: "none", "subtle", "smooth", "bouncy"
300
+ * - *none*: No animation
301
+ * - *subtle*: Fade only (no movement)
302
+ * - *smooth*: Fade + slide animation
303
+ * - *bouncy*: Spring-like bounce effect
304
+ *
305
+ * @example
306
+ * validateAnimationStyle("subtle"); // ✅ true
307
+ * validateAnimationStyle("SMOOTH"); // ✅ true (case-insensitive)
308
+ * validateAnimationStyle("bouncy"); // ✅ true
309
+ * validateAnimationStyle("custom"); // ❌ false (invalid)
310
+ */
311
+ function validateAnimationStyle(style) {
312
+ const validStyles = ["none", "subtle", "smooth", "bouncy"];
313
+ return validStyles.includes(style.toLowerCase());
314
+ }
315
+ /**
316
+ * Validate merchant authentication key
317
+ *
318
+ * @param {string} merchantKey - The merchant key to validate
319
+ * @returns {boolean} True if merchant key is valid, false otherwise
320
+ *
321
+ * @description
322
+ * Validates that a merchant key is properly formatted for authentication.
323
+ * - Must be a non-empty string
324
+ * - Maximum length of 500 characters
325
+ * - Cannot be whitespace-only
326
+ *
327
+ * @example
328
+ * validateMerchantKey("pk_live_abc123xyz"); // ✅ true
329
+ * validateMerchantKey(""); // ❌ false (empty)
330
+ * validateMerchantKey(" "); // ❌ false (whitespace)
331
+ */
332
+ function validateMerchantKey(merchantKey) {
333
+ if (!merchantKey || typeof merchantKey !== "string") {
334
+ return false;
335
+ }
336
+ // Non-empty string, max 500 chars
337
+ if (merchantKey.trim().length === 0 || merchantKey.length > 500) {
338
+ return false;
339
+ }
340
+ return true;
341
+ }
342
+ /**
343
+ * Validate session authentication token
344
+ *
345
+ * @param {string | Function} token - Session token string or function
346
+ * @returns {boolean} True if token is valid, false otherwise
347
+ *
348
+ * @description
349
+ * Validates that a session token is either:
350
+ * - A non-empty string token, OR
351
+ * - A function that returns a string or Promise<string> for async token retrieval
352
+ *
353
+ * This allows both static tokens and dynamic token providers.
354
+ *
355
+ * @example
356
+ * validateSessionToken("token_abc123"); // ✅ true
357
+ * validateSessionToken(() => "dynamic_token"); // ✅ true
358
+ * validateSessionToken(async () => await fetchToken()); // ✅ true
359
+ * validateSessionToken(""); // ❌ false (empty string)
360
+ * validateSessionToken({} as any); // ❌ false (not function)
361
+ */
362
+ function validateSessionToken(token) {
363
+ if (typeof token === "string") {
364
+ return token.trim().length > 0;
365
+ }
366
+ if (typeof token === "function") {
367
+ return true;
368
+ }
369
+ return false;
370
+ }
21
371
 
22
372
  /**
23
373
  * Manages SDK configuration with validation and defaults
@@ -26,10 +376,28 @@ class ConfigManager {
26
376
  constructor() {
27
377
  this.config = null;
28
378
  }
379
+ /**
380
+ * Get API base URL override from environment (if provided)
381
+ * Supports both browser global and build-time env injection
382
+ */
383
+ static getApiBaseUrlOverride() {
384
+ var _a, _b;
385
+ const globalValue = globalThis
386
+ .COBUY_API_BASE_URL;
387
+ const envValue = (_b = (_a = globalThis.process) === null || _a === void 0 ? void 0 : _a.env) === null || _b === void 0 ? void 0 : _b.COBUY_API_BASE_URL;
388
+ const override = globalValue || envValue;
389
+ if (override && typeof override === "string") {
390
+ return override.trim();
391
+ }
392
+ return null;
393
+ }
29
394
  /**
30
395
  * Set and validate SDK configuration
396
+ *
397
+ * @throws {CoBuyInvalidConfigError} If authMode, merchantKey, or sessionToken validation fails
31
398
  */
32
399
  setConfig(options) {
400
+ var _a, _b, _c, _d, _e, _f;
33
401
  // Determine auth mode (default: public)
34
402
  const authMode = options.authMode || "public";
35
403
  // Validate authentication configuration based on mode
@@ -37,6 +405,10 @@ class ConfigManager {
37
405
  if (!options.merchantKey || typeof options.merchantKey !== "string") {
38
406
  throw new CoBuyInvalidConfigError('authMode="public" requires a valid merchantKey string');
39
407
  }
408
+ // Validate merchant key format
409
+ if (!validateMerchantKey(options.merchantKey)) {
410
+ throw new CoBuyInvalidConfigError("Invalid merchantKey format. Must be a non-empty string (max 500 chars)");
411
+ }
40
412
  }
41
413
  else if (authMode === "token") {
42
414
  if (!options.sessionToken) {
@@ -45,18 +417,29 @@ class ConfigManager {
45
417
  if (typeof options.sessionToken !== "string" && typeof options.sessionToken !== "function") {
46
418
  throw new CoBuyInvalidConfigError("sessionToken must be a string or function that returns a string/Promise<string>");
47
419
  }
420
+ // Validate session token format
421
+ if (!validateSessionToken(options.sessionToken)) {
422
+ throw new CoBuyInvalidConfigError("Invalid sessionToken format. Must be a non-empty string or valid function");
423
+ }
48
424
  }
49
425
  else {
50
426
  throw new CoBuyInvalidConfigError(`Invalid authMode: ${authMode}. Must be "public" or "token"`);
51
427
  }
52
428
  // Set environment with default
53
429
  const env = options.env || "production";
54
- // Set API base URL based on environment (computed internally)
55
- const apiBaseUrl = ConfigManager.DEFAULT_API_URLS[env];
430
+ // Set API base URL based on environment, allow env override
431
+ const envOverride = ConfigManager.getApiBaseUrlOverride();
432
+ const apiBaseUrl = envOverride || ConfigManager.DEFAULT_API_URLS[env];
56
433
  // Theme configuration
57
434
  const theme = options.theme || {};
58
435
  // Debug flag
59
436
  const debug = options.debug || false;
437
+ // Performance configuration with defaults
438
+ const performance = {
439
+ requestTimeout: (_b = (_a = options.performance) === null || _a === void 0 ? void 0 : _a.requestTimeout) !== null && _b !== void 0 ? _b : 30000, // Default: 30 seconds
440
+ maxRetries: (_d = (_c = options.performance) === null || _c === void 0 ? void 0 : _c.maxRetries) !== null && _d !== void 0 ? _d : 2, // Default: 2 retries
441
+ animationSpeed: (_f = (_e = options.performance) === null || _e === void 0 ? void 0 : _e.animationSpeed) !== null && _f !== void 0 ? _f : "normal", // Default: normal speed
442
+ };
60
443
  // Create and store internal config
61
444
  this.config = {
62
445
  authMode,
@@ -68,16 +451,17 @@ class ConfigManager {
68
451
  theme,
69
452
  debug,
70
453
  events: options.events,
454
+ performance,
71
455
  };
72
456
  return this.config;
73
457
  }
74
458
  /**
75
459
  * Get current configuration
76
- * @throws CoBuyNotInitializedError if SDK has not been initialized
460
+ * @throws {CoBuyNotInitializedError} if SDK has not been initialized
77
461
  */
78
462
  getConfig() {
79
463
  if (!this.config) {
80
- throw new Error("CoBuy SDK has not been initialized. Call CoBuy.init(options) first.");
464
+ throw new CoBuyNotInitializedError();
81
465
  }
82
466
  return this.config;
83
467
  }
@@ -172,28 +556,161 @@ const ANIMATION_STYLES = {
172
556
  bouncy: { easing: "cubic-bezier(0.68, -0.55, 0.265, 1.55)", transform: "translateY(12px)" }, // Spring
173
557
  };
174
558
  /**
175
- * Merge global theme with optional widget-specific overrides
176
- * Priority: widgetTheme > globalTheme > defaults
559
+ * Merge global theme with widget-specific theme overrides
560
+ *
561
+ * Combines multiple theme objects with priority: widgetTheme > globalTheme > defaults.
562
+ * Validates all color and style values, falling back to defaults if invalid.
563
+ *
564
+ * @param {CoBuyThemeOptions} [globalTheme={}] - Global theme to apply to all widgets
565
+ * @param {string} [globalTheme.primaryColor] - Primary brand color (validated as CSS color)
566
+ * @param {string} [globalTheme.secondaryColor] - Secondary brand color
567
+ * @param {string} [globalTheme.textColor] - Text color for widget
568
+ * @param {string} [globalTheme.borderRadius] - Border radius for widget elements (CSS size value)
569
+ * @param {string} [globalTheme.fontFamily] - Font family for widget text
570
+ * @param {"none" | "fast" | "normal" | "slow"} [globalTheme.animationSpeed] - Animation speed
571
+ * @param {"none" | "subtle" | "smooth" | "bouncy"} [globalTheme.animationStyle] - Animation style
572
+ *
573
+ * @param {CoBuyThemeOptions} [widgetTheme={}] - Widget-specific theme overrides (takes priority over global)
574
+ * @param {string} [widgetTheme.primaryColor] - Override primary color
575
+ * @param {string} [widgetTheme.secondaryColor] - Override secondary color
576
+ * @param {string} [widgetTheme.textColor] - Override text color
577
+ * @param {string} [widgetTheme.borderRadius] - Override border radius
578
+ * @param {string} [widgetTheme.fontFamily] - Override font family
579
+ * @param {"none" | "fast" | "normal" | "slow"} [widgetTheme.animationSpeed] - Override animation speed
580
+ * @param {"none" | "subtle" | "smooth" | "bouncy"} [widgetTheme.animationStyle] - Override animation style
581
+ *
582
+ * @returns {Required<CoBuyThemeOptions>} Merged theme with all properties defined
583
+ *
584
+ * @description
585
+ * All theme values are validated:
586
+ * - Colors are checked against valid CSS color formats
587
+ * - Border radius is validated as a CSS size value
588
+ * - Font family is validated as a non-empty string
589
+ * - Animation speeds and styles are checked against allowed values
590
+ * - Invalid values are logged and replaced with defaults
591
+ *
592
+ * @example
593
+ * ```typescript
594
+ * // Merge global and widget themes
595
+ * const theme = mergeTheme(
596
+ * {
597
+ * primaryColor: '#4f46e5',
598
+ * fontFamily: 'Inter, sans-serif'
599
+ * },
600
+ * {
601
+ * primaryColor: '#ff0000' // Override primary color for this widget
602
+ * }
603
+ * );
604
+ * // Result: theme.primaryColor === '#ff0000'
605
+ *
606
+ * // With invalid color (falls back to default)
607
+ * const theme2 = mergeTheme(
608
+ * { primaryColor: 'notacolor' },
609
+ * {}
610
+ * );
611
+ * // Result: theme2.primaryColor === '#4f46e5' (default value)
612
+ * ```
177
613
  */
178
614
  function mergeTheme(globalTheme = {}, widgetTheme = {}) {
179
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
615
+ var _a, _b, _c, _d, _e, _f, _g;
616
+ const logger = new Logger(false);
617
+ // Helper to validate and fallback to default
618
+ const getValidColor = (value, fallback) => {
619
+ if (!value)
620
+ return fallback;
621
+ if (validateColor(value))
622
+ return value;
623
+ logger.warn(`Invalid color value: ${value}, using default: ${fallback}`);
624
+ return fallback;
625
+ };
626
+ const getValidBorderRadius = (value, fallback) => {
627
+ if (!value)
628
+ return fallback;
629
+ if (validateBorderRadius(value))
630
+ return value;
631
+ logger.warn(`Invalid border radius: ${value}, using default: ${fallback}`);
632
+ return fallback;
633
+ };
634
+ const getValidFontFamily = (value, fallback) => {
635
+ if (!value)
636
+ return fallback;
637
+ if (validateFontFamily(value))
638
+ return value;
639
+ logger.warn(`Invalid font family: ${value}, using default: ${fallback}`);
640
+ return fallback;
641
+ };
642
+ const getValidAnimationSpeed = (value, fallback) => {
643
+ if (!value)
644
+ return fallback;
645
+ if (validateAnimationSpeed(value))
646
+ return value;
647
+ logger.warn(`Invalid animation speed: ${value}, using default: ${fallback}`);
648
+ return fallback;
649
+ };
650
+ const getValidAnimationStyle = (value, fallback) => {
651
+ if (!value)
652
+ return fallback;
653
+ if (validateAnimationStyle(value))
654
+ return value;
655
+ logger.warn(`Invalid animation style: ${value}, using default: ${fallback}`);
656
+ return fallback;
657
+ };
180
658
  return {
181
- primaryColor: (_b = (_a = widgetTheme.primaryColor) !== null && _a !== void 0 ? _a : globalTheme.primaryColor) !== null && _b !== void 0 ? _b : DEFAULT_THEME.primaryColor,
182
- secondaryColor: (_d = (_c = widgetTheme.secondaryColor) !== null && _c !== void 0 ? _c : globalTheme.secondaryColor) !== null && _d !== void 0 ? _d : DEFAULT_THEME.secondaryColor,
183
- textColor: (_f = (_e = widgetTheme.textColor) !== null && _e !== void 0 ? _e : globalTheme.textColor) !== null && _f !== void 0 ? _f : DEFAULT_THEME.textColor,
184
- borderRadius: (_h = (_g = widgetTheme.borderRadius) !== null && _g !== void 0 ? _g : globalTheme.borderRadius) !== null && _h !== void 0 ? _h : DEFAULT_THEME.borderRadius,
185
- fontFamily: (_k = (_j = widgetTheme.fontFamily) !== null && _j !== void 0 ? _j : globalTheme.fontFamily) !== null && _k !== void 0 ? _k : DEFAULT_THEME.fontFamily,
186
- animationSpeed: (_m = (_l = widgetTheme.animationSpeed) !== null && _l !== void 0 ? _l : globalTheme.animationSpeed) !== null && _m !== void 0 ? _m : DEFAULT_THEME.animationSpeed,
187
- animationStyle: (_p = (_o = widgetTheme.animationStyle) !== null && _o !== void 0 ? _o : globalTheme.animationStyle) !== null && _p !== void 0 ? _p : DEFAULT_THEME.animationStyle,
659
+ primaryColor: getValidColor((_a = widgetTheme.primaryColor) !== null && _a !== void 0 ? _a : globalTheme.primaryColor, DEFAULT_THEME.primaryColor),
660
+ secondaryColor: getValidColor((_b = widgetTheme.secondaryColor) !== null && _b !== void 0 ? _b : globalTheme.secondaryColor, DEFAULT_THEME.secondaryColor),
661
+ textColor: getValidColor((_c = widgetTheme.textColor) !== null && _c !== void 0 ? _c : globalTheme.textColor, DEFAULT_THEME.textColor),
662
+ borderRadius: getValidBorderRadius((_d = widgetTheme.borderRadius) !== null && _d !== void 0 ? _d : globalTheme.borderRadius, DEFAULT_THEME.borderRadius),
663
+ fontFamily: getValidFontFamily((_e = widgetTheme.fontFamily) !== null && _e !== void 0 ? _e : globalTheme.fontFamily, DEFAULT_THEME.fontFamily),
664
+ animationSpeed: getValidAnimationSpeed((_f = widgetTheme.animationSpeed) !== null && _f !== void 0 ? _f : globalTheme.animationSpeed, DEFAULT_THEME.animationSpeed),
665
+ animationStyle: getValidAnimationStyle((_g = widgetTheme.animationStyle) !== null && _g !== void 0 ? _g : globalTheme.animationStyle, DEFAULT_THEME.animationStyle),
188
666
  };
189
667
  }
190
668
  /**
191
- * Apply theme to widget container using scoped CSS custom properties
192
- * This approach ensures:
193
- * 1. Styles are isolated to this widget instance
194
- * 2. No conflicts with merchant's global styles
195
- * 3. Easy runtime theme updates
196
- * 4. Browser handles invalid values gracefully
669
+ * Apply theme to widget container using CSS custom properties
670
+ *
671
+ * Sets scoped CSS variables on the container element to apply theme colors,
672
+ * sizing, fonts, and animations. This approach isolates styles to the widget
673
+ * instance and prevents conflicts with merchant's global styles.
674
+ *
675
+ * @param {HTMLElement} container - The DOM element to apply theme to
676
+ * @param {Required<CoBuyThemeOptions>} theme - The theme configuration to apply
677
+ * @param {string} theme.primaryColor - Primary brand color (applied to --cobuy-primary-color)
678
+ * @param {string} theme.secondaryColor - Secondary color (applied to --cobuy-secondary-color)
679
+ * @param {string} theme.textColor - Text color (applied to --cobuy-text-color)
680
+ * @param {string} theme.borderRadius - Border radius (applied to --cobuy-border-radius)
681
+ * @param {string} theme.fontFamily - Font family (applied to --cobuy-font-family)
682
+ * @param {AnimationSpeed} theme.animationSpeed - Animation speed for transitions
683
+ * @param {AnimationStyle} theme.animationStyle - Animation style/easing
684
+ *
685
+ * @returns {void}
686
+ *
687
+ * @description
688
+ * Sets the following CSS custom properties on the container:
689
+ * - `--cobuy-primary-color`: Primary brand color
690
+ * - `--cobuy-secondary-color`: Secondary color
691
+ * - `--cobuy-text-color`: Text color
692
+ * - `--cobuy-border-radius`: Border radius value
693
+ * - `--cobuy-font-family`: Font family stack
694
+ * - `--cobuy-animation-duration`: Duration in milliseconds
695
+ * - `--cobuy-animation-easing`: CSS easing function
696
+ * - `--cobuy-animation-transform`: CSS transform for animations
697
+ *
698
+ * @example
699
+ * ```typescript
700
+ * const container = document.getElementById('cobuy-widget');
701
+ * const theme = {
702
+ * primaryColor: '#4f46e5',
703
+ * secondaryColor: '#6366f1',
704
+ * textColor: '#111827',
705
+ * borderRadius: '8px',
706
+ * fontFamily: 'Inter, sans-serif',
707
+ * animationSpeed: 'normal',
708
+ * animationStyle: 'smooth'
709
+ * };
710
+ *
711
+ * applyTheme(container, theme);
712
+ * // Now all widget styles use the applied theme colors and animations
713
+ * ```
197
714
  */
198
715
  function applyTheme(container, theme) {
199
716
  // Apply color and style CSS variables
@@ -705,8 +1222,26 @@ class GroupListModal {
705
1222
  createHeader() {
706
1223
  const header = document.createElement("div");
707
1224
  header.className = "cobuy-group-list-header";
1225
+ // Create logo container
1226
+ const logoContainer = document.createElement("div");
1227
+ logoContainer.className = "cobuy-group-list-logo-container";
1228
+ logoContainer.innerHTML = `
1229
+ <svg width="140" height="42" viewBox="0 0 168 50" fill="none" xmlns="http://www.w3.org/2000/svg" class="cobuy-group-list-logo">
1230
+ <path d="M3.72838 4.40626H47.9605V44.91H3.72838V4.40626Z" fill="#FF8A15"/>
1231
+ <path d="M5.82983 0.35607C3.45722 1.19213 1.8077 2.75127 0.632695 5.28205L0 6.61523V43.0178L0.858657 44.7351C1.85289 46.7461 3.47982 48.3731 5.49089 49.3673L6.77887 50H44.7406L45.9833 49.4125C47.7684 48.5764 49.3728 47.1981 50.2088 45.7971C51.655 43.3793 51.6324 43.6957 51.6324 24.4663C51.6324 5.50801 51.655 5.77917 50.367 3.74551C49.531 2.41233 48.1074 1.10175 46.9324 0.559436L45.8704 0.0623187L26.4376 0.0171262C8.22503 -0.0280662 6.93705 -0.00546998 5.82983 0.35607ZM24.4265 8.53591C25.466 8.98783 27.0025 10.095 27.7482 10.9085C27.9741 11.1571 28.4939 12.0157 28.878 12.8292C29.5559 14.2302 29.6011 14.4335 29.6011 16.8965C29.6011 19.2013 29.5333 19.6307 29.0362 20.6249C28.3131 22.1388 26.528 23.8562 24.7655 24.7374C23.4097 25.4379 23.1837 25.4831 21.0145 25.4831C18.8453 25.4831 18.6193 25.4379 17.2861 24.7374C14.3034 23.2235 12.6087 21.0768 11.976 18.0037C11.5693 16.1056 11.7274 15.0436 12.6991 12.8292C13.6255 10.7051 16.3597 8.62629 18.9356 8.08398C20.3818 7.76764 23.2515 7.9936 24.4265 8.53591ZM37.9391 24.3307C39.4756 25.0989 40.6958 26.3869 41.5093 28.1042C42.0968 29.3922 42.1872 29.7538 42.0968 31.087C41.916 34.0245 40.3569 36.4197 37.8035 37.5721C35.9506 38.4081 34.0299 38.5437 32.1996 37.911C30.4371 37.3235 29.5107 36.6456 28.5842 35.3125C26.4602 32.2394 26.7313 28.6466 29.2847 25.8672C30.7535 24.2855 32.403 23.6528 34.9112 23.7206C36.4025 23.7658 37.0804 23.9239 37.9391 24.3307ZM18.0996 28.8951C19.5006 29.2567 21.2631 30.8836 21.9861 32.4879C22.4155 33.4144 22.551 34.1374 22.5736 35.3125C22.5962 39.2216 19.6361 41.9557 15.5914 41.7072C13.7611 41.5942 12.8121 41.1649 11.4563 39.8769C9.24186 37.7302 8.94811 33.7081 10.8462 31.2451C12.4053 29.1889 15.388 28.2172 18.0996 28.8951Z" fill="#0157AA"/>
1232
+ <path d="M70.6584 13.8234C67.4271 14.6143 63.8795 17.4388 62.5238 20.3085C61.4165 22.6586 61.055 24.5114 61.1906 27.4038C61.3714 31.3355 62.5238 33.9115 65.2579 36.4875C69.2574 40.2158 75.1325 40.9841 80.307 38.4307C81.4142 37.8658 82.4536 37.1427 83.2671 36.3067C84.7133 34.8379 85.9561 32.7817 85.4815 32.6009C85.3234 32.5557 84.1936 32.149 82.996 31.697L80.8041 30.9288L80.1036 31.923C78.3185 34.499 74.8613 35.7418 71.8786 34.8831C69.4834 34.1826 68.105 32.985 67.0204 30.6802C66.4781 29.5504 66.4329 29.1889 66.4329 26.6129C66.4329 23.9691 66.4781 23.698 67.0656 22.5004C67.8565 20.8735 69.6868 19.1787 71.2459 18.6138C73.0762 17.9811 75.4036 18.0263 76.9854 18.7494C78.4089 19.4047 79.9003 20.8057 80.3296 21.9129C80.5104 22.3196 80.6459 22.6812 80.6911 22.7263C80.8041 22.8845 85.3008 20.896 85.3008 20.6927C85.3008 20.5571 84.9844 19.8566 84.5777 19.1109C83.2445 16.6028 80.0358 14.2754 77.1887 13.733C75.5392 13.4167 72.1272 13.4845 70.6584 13.8234Z" fill="#032341"/>
1233
+ <path d="M107.897 26.7259V39.5153L114.405 39.4476C120.641 39.3798 120.935 39.3572 122.246 38.8375C124.212 38.0466 126.042 36.1485 126.562 34.4086C127.014 32.8269 126.901 30.7028 126.313 29.5504C125.771 28.4658 124.46 27.11 123.398 26.5677L122.517 26.1158L123.172 25.7316C123.534 25.5057 124.144 24.9634 124.528 24.534C127.285 21.3932 125.839 16.7157 121.477 14.6595C120.212 14.072 120.167 14.072 114.066 14.0042L107.897 13.9364V26.7259ZM119.105 18.6816C120.438 19.2917 121.003 20.1052 121.003 21.461C121.003 22.4326 120.89 22.7037 120.257 23.3364C119.286 24.3081 118.224 24.5792 115.309 24.5792H112.868V21.4158V18.2523H115.535C117.613 18.2523 118.382 18.3427 119.105 18.6816ZM119.263 28.8951C119.76 29.0307 120.506 29.4826 120.958 29.8893C121.703 30.5672 121.794 30.7706 121.794 31.7648C121.794 33.1884 121.342 34.0019 120.212 34.5894C119.421 34.9961 118.901 35.0639 116.145 35.0639L112.981 35.0865L112.913 31.8552L112.846 28.6466H115.625C117.162 28.6466 118.788 28.7595 119.263 28.8951Z" fill="#032341"/>
1234
+ <path d="M93.2773 20.8509C92.622 21.0316 91.6729 21.3932 91.1758 21.6417C89.8426 22.3196 87.9445 24.3533 87.1989 25.8446C86.5888 27.11 86.5436 27.336 86.5436 30.1153C86.5436 32.9624 86.5662 33.098 87.2667 34.5894C89.6167 39.5153 95.6047 41.3456 100.711 38.6567C102.564 37.6851 103.852 36.3293 104.824 34.2956C105.479 32.9172 105.524 32.6687 105.524 30.2283C105.524 27.7879 105.479 27.5393 104.824 26.1384C102.722 21.7095 98.0451 19.5855 93.2773 20.8509ZM98.61 25.6413C99.8302 26.4095 100.598 27.7653 100.802 29.5504C101.073 31.9682 100.147 33.7985 98.1806 34.8153C97.1186 35.3576 96.7571 35.448 95.6273 35.3576C93.0739 35.1317 91.4696 33.5273 91.2436 30.9288C90.8143 26.3417 94.9494 23.3816 98.61 25.6413Z" fill="#032341"/>
1235
+ <path d="M129.409 27.9009L129.476 35.0865L130.109 36.3745C131.329 38.8826 133.679 40.148 136.459 39.8317C138.131 39.6509 139.396 39.1312 140.503 38.137L141.339 37.4139V38.4533V39.4927H143.938H146.537V30.1153V20.7379H144.006H141.452L141.52 26.3191C141.611 32.3297 141.498 33.1206 140.526 34.2956C139.69 35.2673 138.741 35.6966 137.453 35.6966C136.549 35.6966 136.165 35.561 135.51 35.0413C135.058 34.6572 134.561 34.0245 134.402 33.5951C134.222 33.0754 134.131 30.861 134.131 26.7711L134.109 20.7379H131.713H129.341L129.409 27.9009Z" fill="#032341"/>
1236
+ <path d="M148.322 21.235C148.435 21.5287 149 22.9297 149.587 24.3533C150.175 25.7768 150.852 27.449 151.078 28.0816C151.282 28.6917 152.389 31.4937 153.519 34.2956L155.575 39.3572L155.123 40.5096C154.875 41.1197 154.242 42.837 153.677 44.3057L152.683 46.9721L155.168 46.9043L157.654 46.8365L159.281 42.7692C160.863 38.9052 167.28 22.6811 167.754 21.348L168.003 20.7379H165.337H162.693L161.699 23.4946C159.575 29.5052 157.993 33.2788 157.835 32.8269C156.479 29.1889 154.603 24.0595 154.084 22.5908L153.428 20.7379H150.785H148.118L148.322 21.235Z" fill="#032341"/>
1237
+ </svg>
1238
+ `;
708
1239
  const title = document.createElement("h2");
709
1240
  title.textContent = "Active Groups";
1241
+ const titleContainer = document.createElement("div");
1242
+ titleContainer.className = "cobuy-group-list-title-container";
1243
+ titleContainer.appendChild(logoContainer);
1244
+ titleContainer.appendChild(title);
710
1245
  const actions = document.createElement("div");
711
1246
  actions.className = "cobuy-group-list-actions";
712
1247
  const liveBox = this.createLiveBox();
@@ -738,9 +1273,9 @@ class GroupListModal {
738
1273
  </svg>
739
1274
  `;
740
1275
  closeBtn.addEventListener("click", () => this.close());
741
- header.appendChild(title);
1276
+ actions.appendChild(closeBtn);
1277
+ header.appendChild(titleContainer);
742
1278
  header.appendChild(actions);
743
- header.appendChild(closeBtn);
744
1279
  return header;
745
1280
  }
746
1281
  createLiveBox() {
@@ -788,7 +1323,7 @@ class GroupListModal {
788
1323
  el.style.opacity = disabled ? "0.5" : "1";
789
1324
  el.style.cursor = disabled ? "not-allowed" : "pointer";
790
1325
  el.setAttribute("aria-disabled", disabled ? "true" : "false");
791
- if (typeof el.disabled !== "undefined") {
1326
+ if (el.disabled !== undefined) {
792
1327
  el.disabled = disabled;
793
1328
  }
794
1329
  });
@@ -818,7 +1353,7 @@ class GroupListModal {
818
1353
  createGroupCard(group) {
819
1354
  const card = document.createElement("div");
820
1355
  card.className = "cobuy-group-card";
821
- card.setAttribute("data-group-id", group.name || group.groupId);
1356
+ card.dataset.groupId = group.name || group.groupId;
822
1357
  const header = document.createElement("div");
823
1358
  header.className = "cobuy-group-card-header";
824
1359
  const groupId = document.createElement("div");
@@ -975,7 +1510,7 @@ class GroupListModal {
975
1510
 
976
1511
  .cobuy-group-list-header {
977
1512
  display: flex;
978
- align-items: center;
1513
+ align-items: flex-start;
979
1514
  gap: 18px;
980
1515
  margin-bottom: 26px;
981
1516
  position: relative;
@@ -989,6 +1524,25 @@ class GroupListModal {
989
1524
  margin: 0;
990
1525
  }
991
1526
 
1527
+ .cobuy-group-list-title-container {
1528
+ display: flex;
1529
+ flex-direction: column;
1530
+ align-items: flex-start;
1531
+ gap: 12px;
1532
+ }
1533
+
1534
+ .cobuy-group-list-logo-container {
1535
+ display: flex;
1536
+ align-items: center;
1537
+ flex-shrink: 0;
1538
+ }
1539
+
1540
+ .cobuy-group-list-logo {
1541
+ height: 42px;
1542
+ width: auto;
1543
+ display: block;
1544
+ }
1545
+
992
1546
  .cobuy-group-list-actions {
993
1547
  margin-left: auto;
994
1548
  margin-right: 48px;
@@ -1317,6 +1871,17 @@ class GroupListModal {
1317
1871
  padding-right: 0;
1318
1872
  }
1319
1873
 
1874
+ .cobuy-group-list-title-container {
1875
+ flex-direction: column;
1876
+ align-items: flex-start;
1877
+ gap: 12px;
1878
+ width: 100%;
1879
+ }
1880
+
1881
+ .cobuy-group-list-logo {
1882
+ height: 36px;
1883
+ }
1884
+
1320
1885
  .cobuy-group-list-header h2 {
1321
1886
  font-size: 28px;
1322
1887
  }
@@ -1514,41 +2079,12 @@ function isRedemptionValid(expiryDate) {
1514
2079
  }
1515
2080
  }
1516
2081
 
1517
- function styleInject(css, ref) {
1518
- if ( ref === void 0 ) ref = {};
1519
- var insertAt = ref.insertAt;
1520
-
1521
- if (typeof document === 'undefined') { return; }
1522
-
1523
- var head = document.head || document.getElementsByTagName('head')[0];
1524
- var style = document.createElement('style');
1525
- style.type = 'text/css';
1526
-
1527
- if (insertAt === 'top') {
1528
- if (head.firstChild) {
1529
- head.insertBefore(style, head.firstChild);
1530
- } else {
1531
- head.appendChild(style);
1532
- }
1533
- } else {
1534
- head.appendChild(style);
1535
- }
1536
-
1537
- if (style.styleSheet) {
1538
- style.styleSheet.cssText = css;
1539
- } else {
1540
- style.appendChild(document.createTextNode(css));
1541
- }
1542
- }
1543
-
1544
- var css_248z = ".cb-lobby-modal-container{height:100%;left:0;position:fixed;top:0;width:100%;z-index:10000}.cb-lobby-modal-container *{box-sizing:border-box;margin:0;padding:0}.cb-lobby-modal-container body,.cb-lobby-modal-container html{font-family:Inter,sans-serif;height:100%;overflow-x:hidden;width:100%}.cb-lobby-main{backdrop-filter:blur(4px);background-color:color-mix(in oklab,#fff 5%,transparent);border-radius:50px;box-shadow:0 25px 50px -12px #00000040;padding:80px;position:relative;width:100%;z-index:1}.cb-lobby-bg{background-image:url(https://cobuy-dev.s3.af-south-1.amazonaws.com/public/sdk/cb-back-image.png);background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:-1}.cb-lobby-main-wp{align-items:center;display:flex;justify-content:center;padding:40px 80px}.lobby-indicator{backdrop-filter:blur(8px);background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);border-radius:24px;gap:8px;margin-bottom:16px;padding:8px 16px;width:fit-content}.lobby-indicator,.pulsing-dot{align-items:center;display:flex}.pulsing-dot{height:8px;justify-content:center;position:relative;width:8px}.pulsing-dot:after{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.pulsing-dot:after,.pulsing-dot:before{background:#10b981;border-radius:50%;content:\"\";height:100%;position:absolute;width:100%}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.indicator-text{color:#047857;font-size:11px;font-weight:800;letter-spacing:.5px;line-height:1;text-transform:uppercase}.lobby-status-section{display:flex;flex-direction:column;margin-bottom:24px}.lobby-status{background:#fff;border-radius:4px;color:#000;font-size:10px;font-weight:600;line-height:1.2;padding:6px 10px;text-transform:uppercase}.lobby-status.active{background:#155dfc;color:#fff}.lobby-status.complete{background:#10b981;color:#fff}.lobby-status-wp{align-items:center;display:flex;flex-wrap:wrap;gap:10px}.lobby-number{backdrop-filter:blur(3px);background:hsla(0,0%,100%,.2);border-radius:4px;box-shadow:0 25px 50px -12px #00000040;font-size:12px;font-weight:700;letter-spacing:1px;line-height:1.2;padding:5px 8px;text-transform:uppercase}.title-wp{margin-top:25px}.title-wp h2{font-size:60px;font-weight:900;line-height:1;margin-bottom:15px}.sub-title{-webkit-text-fill-color:transparent;animation:gradient-flow 4s linear infinite;background-clip:text;-webkit-background-clip:text;background-image:linear-gradient(90deg,#1e293b,#2563eb 25%,#1e293b 50%);background-size:200% auto;color:#1e293b;font-size:16px;font-weight:700;line-height:1.5;margin-bottom:0}@keyframes gradient-flow{0%{background-position:200% 0}to{background-position:-200% 0}}.sub-title.completed{-webkit-text-fill-color:unset;background-clip:unset;-webkit-background-clip:unset;background-image:none;color:#1e293b}.connected-section{backdrop-filter:blur(4px);background:hsla(0,0%,100%,.05);border:1px solid hsla(0,0%,100%,.1);border-radius:24px;box-shadow:0 1px 2px 0 rgba(0,0,0,.05);display:flex;flex-direction:column;gap:20px;margin-top:24px;padding:24px;transition:all .3s ease}.connected-section:hover{background:hsla(0,0%,100%,.1);box-shadow:0 2px 4px 0 rgba(0,0,0,.08)}.link-share-container{display:flex;flex-direction:column}.link-share-wrapper{align-items:center;display:flex;gap:12px;width:100%}.lobby-link-box{align-items:center;backdrop-filter:blur(8px);background-color:hsla(0,0%,100%,.4);border:1px solid hsla(0,0%,100%,.3);border-radius:14px;box-shadow:0 1px 2px 0 rgba(0,0,0,.05);display:flex;flex:1;gap:16px;justify-content:space-between;min-height:50px;padding:16px 20px;transition:all .2s ease}.lobby-link-box:hover{background-color:hsla(0,0%,100%,.5);box-shadow:0 2px 4px 0 rgba(0,0,0,.08)}.lobby-link-text{color:#71717a;font-size:10px;font-weight:700;letter-spacing:.6px;line-height:1.2;margin-bottom:6px;text-transform:uppercase}.copy-link-btn{align-items:center;background:transparent;border:none;border-radius:6px;color:#64748b;cursor:pointer;display:flex;flex-shrink:0;font-size:13px;font-weight:600;gap:6px;justify-content:center;padding:0;transition:all .2s ease;white-space:nowrap}.copy-link-btn:hover{color:#1e293b}.copy-link-btn .copy-text{display:none}.copy-link-btn svg{flex-shrink:0;height:18px;width:18px}@media (min-width:640px){.copy-link-btn .copy-text{display:inline}}.lobby-link-url{color:#1e293b;font-size:15px;font-weight:700;line-height:1.4;word-break:break-all}.link-box-container{flex:1;min-width:0}.share-btn{align-items:center;background:#1e293b;border:none;border-radius:14px;box-shadow:0 2px 4px 0 rgba(0,0,0,.1);color:#fff;cursor:pointer;display:flex;flex-shrink:0;font-size:15px;font-weight:600;gap:8px;height:50px;justify-content:center;min-width:auto;padding:35px 24px;transition:all .2s ease}.share-btn .share-text{color:#fff;display:inline;font-size:15px}.share-btn svg{color:#fff}@media (max-width:640px){.share-btn{font-size:14px;height:50px;padding:0 18px}.share-btn .share-text{color:#fff;display:inline}}.share-btn:hover{background:#0f172a;box-shadow:0 4px 8px 0 rgba(0,0,0,.15);transform:translateY(-1px)}.share-btn:active{transform:scale(.95)}.share-btn svg{flex-shrink:0;height:18px;width:18px}.lobby-offer-box{align-items:center;backdrop-filter:blur(8px);background-color:rgba(59,130,246,.063);border:1px solid rgba(59,130,246,.125);border-radius:1rem;display:flex;gap:30px;margin-top:30px;padding:15px 20px}.offer-box-icon{align-items:center;background:linear-gradient(135deg,#3b82f6,rgba(59,130,246,.867));border-radius:10px;box-shadow:0 10px 15px -3px rgba(59,130,246,.314);color:#fff;cursor:pointer;display:flex;height:56px;justify-content:center;padding:5px;width:56px}.offer-lock-status{align-items:center;display:flex;gap:6px;margin-bottom:2px}.offer-lock-status span{font-size:14px;font-weight:700;line-height:1}.offer-box-content h3{font-size:30px;font-weight:900;line-height:1.2}.cb-lobby-top{display:grid;gap:100px;grid-template-columns:7fr 5fr}.group-info{backdrop-filter:blur(8px);background-color:color-mix(in oklab,#fff 5%,transparent);border-radius:20px;box-shadow:0 10px 15px -3px #0000001a;padding:30px}.progress-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:14px}.progress-header .title{align-items:center;color:#000;display:flex;font-size:14px;font-weight:900;justify-content:center;letter-spacing:.7px;position:relative}.progress-badge{background:#3b82f6;border-radius:999px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;color:#fff;font-size:13px;font-weight:700;padding:6px 12px}.cb-lobby-modal-container .progress-bar{background:#fff;border-radius:999px;height:14px;overflow:hidden;width:100%}.cb-lobby-modal-container .progress-fill{animation:shimmer 2.7s linear infinite;background:#3b82f6;background-image:linear-gradient(120deg,hsla(0,0%,100%,.15) 25%,hsla(0,0%,100%,.45) 37%,hsla(0,0%,100%,.15) 63%);background-size:200% 100%;border-radius:999px;height:100%;position:relative;transition:width .6s ease;width:0}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.progress-labels{color:#474d56;display:flex;font-size:12px;justify-content:space-between;margin-top:8px}.team-card{margin-top:40px}.team-card .icon-box svg{width:16px}.team-card .team-header{align-items:center;display:flex;justify-content:space-between}.team-card .team-title{align-items:center;display:flex;font-size:14px;font-weight:600;gap:12px;letter-spacing:1px}.team-card .icon-box{align-items:center;background-color:#3b82f6;border-radius:.5rem;box-shadow:0 10px 15px -3px #3b82f64d;color:#fff;display:flex;font-size:18px;height:2rem;justify-content:center;width:2rem}.team-card .team-title span{color:#000;font-size:14px;font-weight:900;letter-spacing:.7px}.team-card .team-count{background-color:#3b82f6;border-radius:9999px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;color:#fff;font-size:13px;font-weight:700;letter-spacing:.7px;padding:6px 12px}.team-card .team-members{align-items:center;display:flex;gap:14px;justify-content:center;margin-top:20px}.team-card .member{border:3px solid #fff;border-radius:50%;box-shadow:0 4px 12px #0000001a;font-size:22px;height:56px;position:relative;width:56px}.team-card .member,.team-card .member:after{align-items:center;color:#fff;display:flex;justify-content:center}.team-card .member:after{background:#3b82f6;background-image:url(https://cobuy-dev.s3.af-south-1.amazonaws.com/public/sdk/tick-mark-icon.svg);background-position:50%;background-repeat:no-repeat;background-size:75%;border:2px solid #fff;border-radius:50%;bottom:-4px;content:\"\";height:12px;padding:2px;position:absolute;right:-4px;width:12px}.team-card .member.blue{background:#2563eb}.team-card .member.purple{background:#9333ea}.team-card .member.pink{background:#ec4899}.team-card .member.orange{background:#f97316}.team-card .member.empty{background:#f8fafc;color:#9ca3af}.team-card .member.empty:after{display:none}.time-card{align-items:center;backdrop-filter:blur(8px);background:hsla(0,0%,100%,.2);border:1px solid hsla(0,0%,100%,.2);border-radius:20px;display:flex;gap:14px;margin-top:30px;padding:15px}.time-card .time-icon{align-items:center;background:#3b82f6;border-radius:14px;color:#fff;display:flex;flex-shrink:0;font-size:22px;height:48px;justify-content:center;width:48px}.time-card .time-content{display:flex;flex-direction:column}.time-card .time-label{color:#4b5563;font-size:12px;font-weight:600;letter-spacing:.6px}.time-card .time-value{color:#111827;font-size:22px;font-weight:900}.group-info-box{backdrop-filter:blur(8px);background:color-mix(in oklab,#fff 5%,transparent);border:1px solid color-mix(in oklab,#fff 20%,transparent);border-radius:1.5rem;box-shadow:0 10px 15px -3px #0000001a;padding:30px}.offer-lock-status svg{height:16px;width:16px}.cb-lobby-modal-container .offer-box-content .reward-text{background:unset;border:none;color:#000;font-size:14px;line-height:1;margin-top:4px}.progress-header .title:before{background:#3b82f6;border-radius:50%;content:\"\";display:inline-block;height:8px;margin-right:8px;position:relative;width:8px}.live-activity-wrapper{backdrop-filter:blur(8px);background-color:color-mix(in oklab,#fff 80%,transparent);border-radius:18px;box-shadow:0 30px 80px rgba(0,0,0,.15);padding:16px}.live-activity-header{display:flex;gap:10px;justify-content:space-between;margin-bottom:12px}.live-activity-header .title{align-items:center;color:#000;display:flex;font-size:14px;font-weight:600;font-weight:900;gap:8px;letter-spacing:.7px}.live-activity-header .dot{background:#3b82f6;border-radius:50%;height:8px;position:relative;width:8px}.live-activity-header .dot:after{animation:livePulse 1.6s ease-out infinite;background:rgba(59,130,246,.6);border-radius:50%;content:\"\";inset:0;position:absolute}@keyframes livePulse{0%{opacity:.8;transform:scale(1)}70%{opacity:0;transform:scale(2.4)}to{opacity:0}}.activity-stats{align-items:center;display:flex;gap:8px}.activity-stats-badge{background-color:rgba(59,130,246,.082);border-radius:999px;color:#3b82f6;font-size:12px;font-weight:500;line-height:1;padding:4px 10px}.activity-stats-badge.light{background-color:#f9f3f4;color:#45556c}.activity-list{height:104px;overflow:hidden;position:relative}.activity-card{align-items:center;background:linear-gradient(90deg,#fff,#f2f6ff);border:1px solid #dbeafe;border-radius:14px;display:flex;gap:10px;height:58px;inset:0;padding:12px 10px;position:absolute;transition:transform .6s cubic-bezier(.22,.61,.36,1),opacity .6s}.activity-card .text{font-size:14px}.activity-card .avatar{align-items:center;border-radius:50%;color:#fff;display:flex;flex:0 0 30px;height:30px;justify-content:center;width:30px}.activity-card .pink{background:linear-gradient(135deg,#ec4899,#f472b6)}.activity-card .purple{background:linear-gradient(135deg,#8b5cf6,#a78bfa)}.activity-card .blue{background:linear-gradient(135deg,#3b82f6,#60a5fa)}.activity-card .green{background:linear-gradient(135deg,#10b981,#34d399)}.activity-card .orange{background:linear-gradient(135deg,#f97316,#fb923c)}.activity-card .text p{color:#6b7280;font-size:12px;margin:2px 0 0}.activity-card .time{color:#94a3b8;font-size:10px;margin-left:auto}.lobby-activity-wp{backdrop-filter:blur(12px);background-color:hsla(0,0%,100%,.4);border:1px solid hsla(0,0%,100%,.2);border-radius:25px;box-shadow:0 1px 3px 0 #0000001a;margin-top:50px;padding:8px}.lobby-close-icon{align-items:center;background-color:color-mix(in oklab,#000 80%,transparent);border-radius:50%;color:#fff;cursor:pointer;display:flex;height:34px;justify-content:center;position:fixed;right:30px;top:30px;width:34px;z-index:99}.lobby-close-icon svg{height:20px;width:20px}@media screen and (max-width:1600px){.cb-lobby-main{border-radius:30px;padding:50px}.title-wp h2{font-size:48px;margin-bottom:12px}.sub-title{font-size:16px}.title-wp{margin-top:20px}.lobby-link-section{margin-top:30px}.offer-box-content h3{font-size:26px}.lobby-offer-box{gap:24px;padding:15px}.cb-lobby-top{gap:80px}.group-info-box{border-radius:20px;padding:22px}.team-card{margin-top:30px}.team-card .team-members{margin-top:12px}.lobby-activity-wp{margin-top:40px}.lobby-close-icon{height:30px;top:20px;width:30px}}@media screen and (max-width:1280px){.cb-lobby-main,.cb-lobby-main-wp{padding:40px}.title-wp h2{font-size:42px}.cb-lobby-top{gap:60px}}@media screen and (max-width:1120px){.title-wp h2{font-size:38px}}@media screen and (max-width:991px){.cb-lobby-top{gap:30px;grid-template-columns:1fr}.cb-lobby-main{border-radius:15px;padding:30px}.cb-lobby-main-wp{padding:30px}.lobby-close-icon{right:20px}.lobby-link-box{border-radius:10px}.share-btn{border-radius:8px}.offer-box-content h3{font-size:24px}.lobby-activity-wp,.lobby-offer-box,.time-card{border-radius:15px;margin-top:20px}.live-activity-wrapper{border-radius:10px}.group-info-box{border-radius:15px}}@media screen and (max-width:575px){.cb-lobby-main,.cb-lobby-main-wp{padding:20px}.title-wp h2{font-size:30px}.lobby-link-text{font-size:11px;margin-bottom:4px}.lobby-link-url{font-size:14px}.lobby-link-box{min-height:48px;padding:12px 16px}.link-share-wrapper{gap:10px}.share-btn{border-radius:8px;font-size:13px;height:48px;min-width:auto;padding:0 14px}.lobby-offer-box{padding:10px}.offer-box-content h3{font-size:20px}.offer-box-content .reward-text{font-size:13px}.group-info-box{padding:13px}.progress-header .title,.team-card .team-title span{font-size:13px}.team-card .member{height:40px;width:40px}.team-card .member:after{height:8px;width:8px}.team-card .team-members{gap:10px;margin-top:12px}.time-card{padding:13px}.team-card{margin-top:22px}.activity-card .text{font-size:12px}.activity-card .text p{font-size:11px}.live-activity-header .title{font-size:12px}.time-card .time-value{font-size:20px}}@media screen and (max-width:480px){.cb-lobby-main-wp{padding:20px 12px}.cb-lobby-main{padding:15px 12px}.lobby-status{font-size:9px}.lobby-number{font-size:10px}.title-wp h2{font-size:28px;margin-bottom:7px}.sub-title{font-size:14px}.lobby-link-section{gap:10px;margin-top:20px}.lobby-offer-box{gap:12px}.offer-box-icon{height:45px;width:45px}.offer-box-content .reward-text,.offer-lock-status span{font-size:12px}.cb-lobby-top{gap:20px}.lobby-close-icon{right:10px;top:10px}.live-activity-header{flex-direction:column;gap:5px}.activity-card{border-radius:9px;padding:6px}}.share-overlay{align-items:center;background:rgba(0,0,0,.45);display:flex;inset:0;justify-content:center;opacity:0;position:fixed;transition:.3s ease;visibility:hidden;z-index:999}.share-overlay.active{opacity:1;visibility:visible}.share-popup{background:#fff;border-radius:18px;max-height:90%;overflow:auto;padding:24px;position:relative;transform:translateY(30px);transition:.35s ease;width:420px}.share-overlay.active .share-popup{transform:translateY(0)}.share-popup .close-btn{align-items:center;background:#f3f4f6;border:none;border-radius:50%;cursor:pointer;display:flex;height:32px;justify-content:center;position:absolute;right:14px;top:14px;transition:.3s;width:32px}.share-popup .close-btn:hover{background:#eeecec}.share-popup h2{font-size:22px;font-weight:600;line-height:1;margin-bottom:10px}.share-popup .subtitle{color:#4a5565;font-size:14px;margin:6px 0 30px}.share-popup .share-grid{display:grid;gap:14px;grid-template-columns:repeat(2,1fr)}.share-popup .share-card{align-items:center;background:#f2f6f9;border-radius:14px;cursor:pointer;display:flex;flex-direction:column;padding:16px;position:relative;text-align:center;transition:.25s ease}.share-popup .share-card:hover{transform:translateY(-3px)}.share-popup .share-card .icon{align-items:center;background:#1877f2;border-radius:50%;color:#fff;display:flex;font-size:30px;height:50px;justify-content:center;margin-bottom:8px;text-align:center;width:50px}.share-popup .share-card span{font-size:13px;font-weight:500}.share-popup .share-card.whatsapp{background:#ecfdf5;border:2px solid #22c55e;color:#22c55e}.share-popup .share-card.whatsapp .icon{background:#22c55e}.share-popup .share-card .badge{background:#2563eb;border-radius:12px;color:#fff;font-size:11px;padding:4px 10px;position:absolute;right:10px;top:-8px}.share-popup .link-box{background:#fbf9fa;border:1px solid #ebe6e7;border-radius:14px;margin-top:18px;padding:14px}.share-popup .link-box label{color:#64748b;font-size:13px}.share-popup .link-row{display:flex;gap:8px;margin-top:6px}.share-popup .link-row input{background:transparent;border:none;color:#334155;flex:1;font-size:13px;letter-spacing:.8px;line-height:1.2;outline:none}.share-popup .link-row button{align-items:center;background:#fff;border:1px solid #ebe6e7;border-radius:10px;cursor:pointer;display:flex;height:35px;justify-content:center;padding:6px 8px;width:35px}.share-popup .success{color:#2563eb;display:block;font-size:12px;margin-top:6px;opacity:0;transition:opacity .3s}.share-popup .success.show{opacity:1}.share-popup .footer-text{color:#64748b;font-size:12px;margin-top:14px;text-align:center}.share-popup .share-card.twitter .icon{background:#000}.share-popup .share-card.facebook .icon{background:#1877f2}.share-popup .share-card.sms .icon{background:#059669}.share-popup .share-card.copied .icon{background:#2563eb}@keyframes entrance-fade-in{0%{opacity:0}to{opacity:1}}@keyframes entrance-scale-in{0%{opacity:0;transform:scale(.9) translateY(20px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes entrance-text-fade-in{0%{opacity:0}to{opacity:1}}@keyframes exit-blur-scale{0%{filter:blur(0);opacity:1;transform:scale(1)}to{filter:blur(10px);opacity:0;transform:scale(1.1)}}@keyframes pulse-dot{0%,to{opacity:1}50%{opacity:.5}}.entrance-animation-overlay{align-items:center;animation:entrance-fade-in .8s ease-in-out forwards;background-color:#0f172a;color:#fff;display:flex;flex-direction:column;inset:0;justify-content:center;position:fixed;z-index:9999}.entrance-animation-overlay.exit{animation:exit-blur-scale .8s ease-in-out forwards}.entrance-content{align-items:center;animation:entrance-scale-in .5s ease-out .2s both;display:flex;flex-direction:column}.entrance-icon-box{align-items:center;background:#2563eb;border-radius:16px;box-shadow:0 10px 25px rgba(37,99,235,.2);display:flex;height:64px;justify-content:center;margin-bottom:24px;width:64px}.entrance-icon-box svg{color:#fff;height:32px;width:32px}.entrance-title{font-size:32px;font-weight:700;letter-spacing:-.5px;margin-bottom:12px}@media (min-width:768px){.entrance-title{font-size:48px}}.entrance-message{align-items:center;animation:entrance-text-fade-in .5s ease-out .5s both;color:#60a5fa;display:flex;font-size:18px;font-weight:500;gap:8px}.entrance-pulse-dot{animation:pulse-dot 1.5s ease-in-out infinite;background-color:#60a5fa;border-radius:50%;height:8px;width:8px}";
1545
- styleInject(css_248z,{"insertAt":"bottom"});
1546
-
2082
+ /// <reference lib="dom" />
1547
2083
  /**
1548
2084
  * LobbyModal - Renders and manages the group buying lobby modal
1549
2085
  */
1550
2086
  class LobbyModal {
1551
- constructor(data, callbacks, apiClient, analyticsClient, socketManager = null, debug = false) {
2087
+ constructor(data, callbacks, apiClient, socketManager = null, debug = false) {
1552
2088
  this.modalElement = null;
1553
2089
  this.timerInterval = null;
1554
2090
  this.activityInterval = null;
@@ -1556,21 +2092,7 @@ class LobbyModal {
1556
2092
  this.socketListenerRegistered = false;
1557
2093
  this.currentGroupId = null;
1558
2094
  this.shareOverlay = null;
1559
- /**
1560
- * Unsubscribe from socket events - testing out flow will remove once we have conviction
1561
- */
1562
- // private unsubscribeFromSocketEvents(): void {
1563
- // if (typeof window === "undefined" || !this.socketListenerRegistered) {
1564
- // return;
1565
- // }
1566
- // window.removeEventListener("group:fulfilled", this.handleSocketGroupUpdate as EventListener);
1567
- // window.removeEventListener(
1568
- // "group:member:joined",
1569
- // this.handleSocketGroupUpdate as EventListener,
1570
- // );
1571
- // window.removeEventListener("group:created", this.handleSocketGroupUpdate as EventListener);
1572
- // this.socketListenerRegistered = false;
1573
- // }
2095
+ this.keyboardHandler = null;
1574
2096
  /**
1575
2097
  * Handle socket group update events
1576
2098
  */
@@ -1661,7 +2183,6 @@ class LobbyModal {
1661
2183
  };
1662
2184
  this.logger = new Logger(debug);
1663
2185
  this.apiClient = apiClient;
1664
- this.analyticsClient = analyticsClient;
1665
2186
  this.socketManager = socketManager;
1666
2187
  // Log the group data being passed into the modal
1667
2188
  this.logger.info("LobbyModal initialized with group data", {
@@ -1708,7 +2229,7 @@ class LobbyModal {
1708
2229
  getTitleText(data) {
1709
2230
  var _a, _b;
1710
2231
  const remainingRaw = ((_a = data.totalMembers) !== null && _a !== void 0 ? _a : 0) - ((_b = data.currentMembers) !== null && _b !== void 0 ? _b : 0);
1711
- const remaining = remainingRaw > 0 ? remainingRaw : 0;
2232
+ const remaining = Math.max(0, remainingRaw);
1712
2233
  const unlocked = !this.computeIsLocked(data);
1713
2234
  if (unlocked) {
1714
2235
  return {
@@ -1836,7 +2357,7 @@ class LobbyModal {
1836
2357
  createModalStructure() {
1837
2358
  const modal = document.createElement("div");
1838
2359
  modal.className = "cb-lobby-modal-container";
1839
- modal.setAttribute("data-product-id", this.data.productId);
2360
+ modal.dataset.productId = this.data.productId;
1840
2361
  // Background
1841
2362
  const background = document.createElement("div");
1842
2363
  background.className = "cb-lobby-bg";
@@ -1922,11 +2443,46 @@ class LobbyModal {
1922
2443
  // New "You are in the lobby" indicator badge
1923
2444
  const lobbyIndicator = document.createElement("div");
1924
2445
  lobbyIndicator.className = "lobby-indicator";
2446
+ // Create logo SVG
2447
+ const logoSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2448
+ logoSvg.setAttribute("viewBox", "0 0 155 46");
2449
+ logoSvg.setAttribute("width", "145");
2450
+ logoSvg.setAttribute("height", "34");
2451
+ logoSvg.setAttribute("class", "lobby-indicator-logo");
2452
+ const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2453
+ path1.setAttribute("d", "M3.43011 4.05376H44.1237V41.3172H3.43011V4.05376Z");
2454
+ path1.setAttribute("fill", "#FF8A15");
2455
+ const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2456
+ path2.setAttribute("d", "M5.36344 0.327584C3.18065 1.09676 1.66308 2.53117 0.582079 4.85949L0 6.08601V39.5763L0.789965 41.1563C1.70466 43.0065 3.20144 44.5032 5.05162 45.4179L6.23656 46H41.1613L42.3047 45.4595C43.947 44.6903 45.423 43.4222 46.1921 42.1333C47.5226 39.909 47.5018 40.2 47.5018 22.509C47.5018 5.06737 47.5226 5.31683 46.3377 3.44586C45.5685 2.21934 44.2588 1.01361 43.1778 0.514681L42.2007 0.0573332L24.3226 0.0157561C7.56703 -0.0258209 6.38208 -0.00503238 5.36344 0.327584ZM22.4724 7.85303C23.4287 8.26881 24.8423 9.28744 25.5283 10.0358C25.7362 10.2645 26.2144 11.0545 26.5678 11.8029C27.1914 13.0917 27.233 13.2788 27.233 15.5448C27.233 17.6652 27.1706 18.0602 26.7133 18.9749C26.048 20.3677 24.4057 21.9477 22.7842 22.7584C21.5369 23.4029 21.329 23.4444 19.3333 23.4444C17.3376 23.4444 17.1298 23.4029 15.9032 22.7584C13.1591 21.3656 11.6 19.3907 11.0179 16.5634C10.6437 14.8172 10.7893 13.8401 11.6832 11.8029C12.5355 9.84873 15.0509 7.93619 17.4208 7.43726C18.7513 7.14622 21.3914 7.35411 22.4724 7.85303ZM34.904 22.3842C36.3176 23.091 37.4402 24.276 38.1886 25.8559C38.7291 27.0409 38.8122 27.3735 38.7291 28.6C38.5627 31.3025 37.1283 33.5061 34.7792 34.5663C33.0746 35.3355 31.3075 35.4602 29.6237 34.8781C28.0022 34.3376 27.1498 33.714 26.2975 32.4875C24.3434 29.6602 24.5928 26.3548 26.9419 23.7978C28.2932 22.3426 29.8108 21.7606 32.1183 21.8229C33.4903 21.8645 34.114 22.01 34.904 22.3842ZM16.6516 26.5835C17.9405 26.9161 19.562 28.4129 20.2273 29.8889C20.6222 30.7412 20.747 31.4064 20.7678 32.4875C20.7885 36.0839 18.0652 38.5993 14.3441 38.3706C12.6602 38.2667 11.7871 37.8717 10.5398 36.6867C8.50251 34.7118 8.23226 31.0115 9.9785 28.7455C11.4129 26.8538 14.157 25.9599 16.6516 26.5835Z");
2457
+ path2.setAttribute("fill", "#0157AA");
2458
+ const path3 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2459
+ path3.setAttribute("d", "M65.0057 12.7176C62.033 13.4451 58.7692 16.0437 57.5219 18.6839C56.5032 20.8459 56.1706 22.5505 56.2953 25.2115C56.4617 28.8287 57.5219 31.1986 60.0373 33.5685C63.7169 36.9986 69.1219 37.7054 73.8825 35.3563C74.9011 34.8366 75.8574 34.1713 76.6058 33.4021C77.9362 32.0509 79.0796 30.1591 78.643 29.9928C78.4975 29.9512 77.4581 29.5771 76.3563 29.1613L74.3398 28.4545L73.6954 29.3692C72.0531 31.7391 68.8724 32.8824 66.1283 32.0925C63.9247 31.448 62.6566 30.3462 61.6588 28.2258C61.1599 27.1864 61.1183 26.8538 61.1183 24.4839C61.1183 22.0516 61.1599 21.8021 61.7004 20.7003C62.428 19.2036 64.1118 17.6444 65.5462 17.1247C67.2301 16.5426 69.3713 16.5842 70.8265 17.2495C72.1362 17.8523 73.5083 19.1412 73.9032 20.1598C74.0695 20.534 74.1943 20.8667 74.2359 20.9082C74.3398 21.0538 78.4767 19.2244 78.4767 19.0373C78.4767 18.9125 78.1857 18.2681 77.8115 17.5821C76.585 15.2745 73.633 13.1333 71.0136 12.6344C69.4961 12.3434 66.357 12.4057 65.0057 12.7176Z");
2460
+ path3.setAttribute("fill", "#032341");
2461
+ const path4 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2462
+ path4.setAttribute("d", "M99.2654 24.5878V36.3541L105.252 36.2918C110.99 36.2294 111.26 36.2086 112.466 35.7305C114.275 35.0029 115.959 33.2566 116.437 31.6559C116.852 30.2007 116.749 28.2466 116.208 27.1864C115.709 26.1885 114.503 24.9412 113.526 24.4423L112.716 24.0265L113.318 23.6731C113.651 23.4652 114.212 22.9663 114.566 22.5713C117.102 19.6817 115.771 15.3785 111.759 13.4867C110.595 12.9462 110.554 12.9462 104.941 12.8839L99.2654 12.8215V24.5878ZM109.576 17.1871C110.803 17.7484 111.323 18.4968 111.323 19.7441C111.323 20.638 111.219 20.8875 110.637 21.4695C109.743 22.3634 108.766 22.6129 106.084 22.6129H103.839V19.7025V16.7921H106.292C108.204 16.7921 108.911 16.8753 109.576 17.1871ZM109.722 26.5835C110.179 26.7082 110.865 27.124 111.281 27.4982C111.967 28.1219 112.05 28.309 112.05 29.2237C112.05 30.5333 111.635 31.2817 110.595 31.8222C109.868 32.1964 109.389 32.2588 106.853 32.2588L103.943 32.2796L103.88 29.3068L103.818 26.3548H106.375C107.789 26.3548 109.285 26.4588 109.722 26.5835Z");
2463
+ path4.setAttribute("fill", "#032341");
2464
+ const path5 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2465
+ path5.setAttribute("d", "M85.8151 19.1827C85.2122 19.349 84.3391 19.6816 83.8818 19.9103C82.6552 20.534 80.909 22.4049 80.223 23.777C79.6617 24.9411 79.6201 25.149 79.6201 27.706C79.6201 30.3254 79.6409 30.4501 80.2854 31.8222C82.4474 36.3541 87.9563 38.0379 92.6545 35.5641C94.3592 34.6702 95.5441 33.4229 96.438 31.5519C97.0409 30.2838 97.0825 30.0551 97.0825 27.81C97.0825 25.5648 97.0409 25.3361 96.438 24.0472C94.5047 19.9727 90.2015 18.0186 85.8151 19.1827ZM90.7212 23.5899C91.8438 24.2967 92.5506 25.544 92.7377 27.1863C92.9872 29.4107 92.1348 31.0946 90.3262 32.03C89.3492 32.529 89.0165 32.6121 87.9771 32.529C85.628 32.3211 84.152 30.8451 83.9441 28.4544C83.5492 24.2343 87.3535 21.511 90.7212 23.5899Z");
2466
+ path5.setAttribute("fill", "#032341");
2467
+ const path6 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2468
+ path6.setAttribute("d", "M119.056 25.6689L119.118 32.2796L119.7 33.4646C120.823 35.7721 122.985 36.9363 125.542 36.6452C127.08 36.4789 128.244 36.0008 129.263 35.0861L130.032 34.4209V35.3771V36.3334H132.423H134.814V27.7062V19.0789H132.485H130.136L130.199 24.2137C130.282 29.7434 130.178 30.471 129.284 31.552C128.515 32.446 127.642 32.8409 126.457 32.8409C125.625 32.8409 125.272 32.7162 124.669 32.2381C124.253 31.8847 123.796 31.3026 123.65 30.9076C123.484 30.4295 123.401 28.3922 123.401 24.6295L123.38 19.0789H121.176H118.994L119.056 25.6689Z");
2469
+ path6.setAttribute("fill", "#032341");
2470
+ const path7 = document.createElementNS("http://www.w3.org/2000/svg", "path");
2471
+ path7.setAttribute("d", "M136.456 19.5361C136.56 19.8063 137.08 21.0952 137.62 22.4049C138.161 23.7146 138.784 25.2529 138.992 25.835C139.179 26.3963 140.198 28.9741 141.237 31.5519L143.129 36.2085L142.713 37.2687C142.485 37.83 141.902 39.4099 141.383 40.7612L140.468 43.2142L142.755 43.1519L145.042 43.0895L146.538 39.3476C147.994 35.7927 153.897 20.8666 154.334 19.64L154.563 19.0787H152.11H149.677L148.763 21.6149C146.809 27.1447 145.353 30.6164 145.208 30.2006C143.961 26.8537 142.235 22.1347 141.757 20.7834L141.154 19.0787H138.722H136.269L136.456 19.5361Z");
2472
+ path7.setAttribute("fill", "#032341");
2473
+ logoSvg.appendChild(path1);
2474
+ logoSvg.appendChild(path2);
2475
+ logoSvg.appendChild(path3);
2476
+ logoSvg.appendChild(path4);
2477
+ logoSvg.appendChild(path5);
2478
+ logoSvg.appendChild(path6);
2479
+ logoSvg.appendChild(path7);
1925
2480
  const pulsingDot = document.createElement("div");
1926
2481
  pulsingDot.className = "pulsing-dot";
1927
2482
  const indicatorText = document.createElement("span");
1928
2483
  indicatorText.className = "indicator-text";
1929
2484
  indicatorText.textContent = "YOU ARE IN THE LOBBY";
2485
+ lobbyIndicator.appendChild(logoSvg);
1930
2486
  lobbyIndicator.appendChild(pulsingDot);
1931
2487
  lobbyIndicator.appendChild(indicatorText);
1932
2488
  // Status row with badge and group number
@@ -2636,7 +3192,6 @@ class LobbyModal {
2636
3192
  const dy = firstRects[i].top - lastRects[i].top;
2637
3193
  card.style.transition = "none";
2638
3194
  card.style.transform = `translate(${dx}px, ${dy}px)`;
2639
- card.offsetHeight; // Force reflow
2640
3195
  card.style.transition = "transform 300ms cubic-bezier(.22,.61,.36,1), opacity 600ms";
2641
3196
  });
2642
3197
  requestAnimationFrame(() => this.layoutActivityCards());
@@ -3004,11 +3559,74 @@ class LobbyModal {
3004
3559
  // Start timers and animations
3005
3560
  this.startTimer();
3006
3561
  this.startActivityAnimation();
3007
- // Track analytics
3008
- if (this.analyticsClient) ;
3009
3562
  this.logger.info(`Lobby modal opened for product: ${this.data.productId}`);
3563
+ // Set up keyboard accessibility (focus trap, ESC to close)
3564
+ this.setupKeyboardAccessibility();
3010
3565
  });
3011
3566
  }
3567
+ /**
3568
+ * Set up keyboard accessibility features: ESC to close and Tab focus trap
3569
+ */
3570
+ setupKeyboardAccessibility() {
3571
+ if (!this.modalElement)
3572
+ return;
3573
+ // ESC key to close modal
3574
+ this.keyboardHandler = (event) => {
3575
+ if (event.key === "Escape" || event.key === "Esc") {
3576
+ event.preventDefault();
3577
+ this.close();
3578
+ return;
3579
+ }
3580
+ // Tab focus trap: keep focus within modal
3581
+ if (event.key === "Tab") {
3582
+ const focusableElements = this.getFocusableElements();
3583
+ if (focusableElements.length === 0)
3584
+ return;
3585
+ const firstElement = focusableElements[0];
3586
+ const lastElement = focusableElements[focusableElements.length - 1];
3587
+ const activeElement = document.activeElement;
3588
+ if (event.shiftKey) {
3589
+ // SHIFT+TAB pressed - move backwards
3590
+ if (activeElement === firstElement) {
3591
+ event.preventDefault();
3592
+ lastElement.focus();
3593
+ }
3594
+ }
3595
+ else if (activeElement === lastElement) {
3596
+ // TAB pressed - move forwards
3597
+ event.preventDefault();
3598
+ firstElement.focus();
3599
+ }
3600
+ }
3601
+ };
3602
+ document.addEventListener("keydown", this.keyboardHandler);
3603
+ this.modalElement.setAttribute("role", "dialog");
3604
+ this.modalElement.setAttribute("aria-modal", "true");
3605
+ this.modalElement.setAttribute("aria-label", "Group lobby");
3606
+ }
3607
+ /**
3608
+ * Get all focusable elements within the modal
3609
+ */
3610
+ getFocusableElements() {
3611
+ if (!this.modalElement)
3612
+ return [];
3613
+ const focusableSelectors = [
3614
+ "button",
3615
+ "a[href]",
3616
+ "input:not([disabled])",
3617
+ "[tabindex]:not([tabindex='-1'])",
3618
+ ];
3619
+ return Array.from(this.modalElement.querySelectorAll(focusableSelectors.join(", ")));
3620
+ }
3621
+ /**
3622
+ * Clean up keyboard event listeners
3623
+ */
3624
+ removeKeyboardAccessibility() {
3625
+ if (this.keyboardHandler) {
3626
+ document.removeEventListener("keydown", this.keyboardHandler);
3627
+ this.keyboardHandler = null;
3628
+ }
3629
+ }
3012
3630
  /**
3013
3631
  * Close the modal
3014
3632
  */
@@ -3020,16 +3638,11 @@ class LobbyModal {
3020
3638
  // Stop timers and animations
3021
3639
  this.stopTimer();
3022
3640
  this.stopActivityAnimation();
3023
- // // Unsubscribe from socket events and group - testing out flow will remove once we have conviction
3024
- // this.unsubscribeFromSocketEvents();
3025
- // if (this.socketManager && this.currentGroupId) {
3026
- // this.socketManager.unsubscribeFromGroup(this.currentGroupId);
3027
- // }
3641
+ // Remove keyboard event listeners
3642
+ this.removeKeyboardAccessibility();
3028
3643
  // Remove modal from DOM
3029
3644
  document.body.removeChild(this.modalElement);
3030
3645
  this.modalElement = null;
3031
- // Track analytics
3032
- if (this.analyticsClient) ;
3033
3646
  // Call callback
3034
3647
  if (this.callbacks.onClose) {
3035
3648
  this.callbacks.onClose();
@@ -3213,8 +3826,8 @@ class LobbyModal {
3213
3826
  "orange",
3214
3827
  ];
3215
3828
  const randomColor = colors[Math.floor(Math.random() * colors.length)];
3216
- let emoji = "👤";
3217
- let action = "joined the group";
3829
+ let emoji;
3830
+ let action;
3218
3831
  switch (eventType) {
3219
3832
  case "group:member:joined":
3220
3833
  emoji = "👤";
@@ -3690,6 +4303,7 @@ class OfflineRedemptionModal {
3690
4303
  }
3691
4304
 
3692
4305
  /// <reference lib="dom" />
4306
+ let skeletonStylesInjected = false;
3693
4307
  /**
3694
4308
  * Widget state enumeration for tracking render lifecycle
3695
4309
  */
@@ -3703,10 +4317,15 @@ var WidgetState;
3703
4317
  * WidgetRoot handles rendering of the CoBuy widget into a DOM container
3704
4318
  */
3705
4319
  class WidgetRoot {
4320
+ /**
4321
+ * Get max retries from config (configurable via performance options)
4322
+ */
4323
+ get MAX_RETRIES() {
4324
+ return this.config.performance.maxRetries;
4325
+ }
3706
4326
  constructor(config, apiClient, analyticsClient = null) {
3707
4327
  this.state = WidgetState.LOADING;
3708
4328
  this.retryCount = 0;
3709
- this.MAX_RETRIES = 1;
3710
4329
  this.currentProductId = null;
3711
4330
  this.currentContainer = null;
3712
4331
  this.debouncedCTAClick = null;
@@ -3725,6 +4344,15 @@ class WidgetRoot {
3725
4344
  this.groupExpiryRefreshTriggered = false; // Track if expiry refresh already triggered for current group
3726
4345
  this.offlineRedemption = null;
3727
4346
  this.offlineRedemptionModal = null;
4347
+ this.isRendering = false;
4348
+ this.renderPromise = null;
4349
+ this.liveRegionAnnouncer = null;
4350
+ // Request deduplication for rapid re-renders
4351
+ this.lastRenderedProductId = null;
4352
+ this.lastRenderedContainer = null;
4353
+ this.renderDebounceTimer = null;
4354
+ this.pendingRenderOptions = null;
4355
+ this.RENDER_DEBOUNCE_MS = 300; // 300ms debounce for rapid re-renders
3728
4356
  /** Handle backend fulfillment notifications */
3729
4357
  this.handleGroupFulfilledEvent = (event) => {
3730
4358
  const detail = event.detail;
@@ -3832,7 +4460,7 @@ class WidgetRoot {
3832
4460
  }
3833
4461
  else if (this.currentGroupData) {
3834
4462
  // Check if data actually changed using participant count
3835
- const currentParticipants = groupContainer.getAttribute("data-participants");
4463
+ const currentParticipants = groupContainer.dataset.participants;
3836
4464
  const newParticipants = String(this.currentGroupData.participants_count || 0);
3837
4465
  if (currentParticipants !== newParticipants) {
3838
4466
  // Data changed, re-render
@@ -3840,7 +4468,7 @@ class WidgetRoot {
3840
4468
  const groupEl = this.renderGroupInfo();
3841
4469
  if (groupEl) {
3842
4470
  // Store current participants for next diff
3843
- groupContainer.setAttribute("data-participants", newParticipants);
4471
+ groupContainer.dataset.participants = newParticipants;
3844
4472
  groupContainer.style.display = "";
3845
4473
  groupContainer.appendChild(groupEl);
3846
4474
  }
@@ -3851,6 +4479,10 @@ class WidgetRoot {
3851
4479
  groupContainer.style.display = "none";
3852
4480
  }
3853
4481
  }
4482
+ else {
4483
+ const selector = typeof options.groupContainer === "string" ? options.groupContainer : "<HTMLElement>";
4484
+ this.logger.warn(`CoBuy: groupContainer not found for ${selector} - skipping update`);
4485
+ }
3854
4486
  }
3855
4487
  if (options.rewardContainer) {
3856
4488
  const rewardContainer = this.resolveContainer(options.rewardContainer);
@@ -3869,6 +4501,10 @@ class WidgetRoot {
3869
4501
  rewardContainer.style.display = "none";
3870
4502
  }
3871
4503
  }
4504
+ else {
4505
+ const selector = typeof options.rewardContainer === "string" ? options.rewardContainer : "<HTMLElement>";
4506
+ this.logger.warn(`CoBuy: rewardContainer not found for ${selector} - skipping update`);
4507
+ }
3872
4508
  }
3873
4509
  // Main widget container should stay hidden if we have no data
3874
4510
  if (!this.currentGroupData && !this.groupFulfilled) {
@@ -3914,6 +4550,12 @@ class WidgetRoot {
3914
4550
  groupContainer.appendChild(groupEl);
3915
4551
  }
3916
4552
  }
4553
+ else {
4554
+ const selector = typeof this.currentOptions.groupContainer === "string"
4555
+ ? this.currentOptions.groupContainer
4556
+ : "<HTMLElement>";
4557
+ this.logger.warn(`CoBuy: groupContainer not found for ${selector} - skipping fulfilled render`);
4558
+ }
3917
4559
  }
3918
4560
  if (this.currentOptions.rewardContainer) {
3919
4561
  const rewardContainer = this.resolveContainer(this.currentOptions.rewardContainer);
@@ -3922,6 +4564,12 @@ class WidgetRoot {
3922
4564
  rewardContainer.innerHTML = "";
3923
4565
  rewardContainer.textContent = rewardText;
3924
4566
  }
4567
+ else {
4568
+ const selector = typeof this.currentOptions.rewardContainer === "string"
4569
+ ? this.currentOptions.rewardContainer
4570
+ : "<HTMLElement>";
4571
+ this.logger.warn(`CoBuy: rewardContainer not found for ${selector} - skipping reward render`);
4572
+ }
3925
4573
  }
3926
4574
  }
3927
4575
  /** Render group info element with progress and countdown */
@@ -3936,8 +4584,8 @@ class WidgetRoot {
3936
4584
  // Match static HTML structure: cobuy-progress container
3937
4585
  const root = document.createElement("div");
3938
4586
  root.className = "cobuy-progress";
3939
- root.setAttribute("data-current", String(participants));
3940
- root.setAttribute("data-target", String(max));
4587
+ root.dataset.current = String(participants);
4588
+ root.dataset.target = String(max);
3941
4589
  // Progress header with two spans
3942
4590
  const header = document.createElement("div");
3943
4591
  header.className = "progress-header";
@@ -3961,7 +4609,7 @@ class WidgetRoot {
3961
4609
  const expiry = Date.parse(data.expiry_at);
3962
4610
  this.updateTimer(progressText, expiry, remaining);
3963
4611
  const interval = window.setInterval(() => this.updateTimer(progressText, expiry, remaining), 1000);
3964
- progressText.setAttribute("data-timer-id", String(interval));
4612
+ progressText.dataset.timerId = String(interval);
3965
4613
  root.appendChild(header);
3966
4614
  root.appendChild(progressBar);
3967
4615
  // Create footer with timer on left and View all Groups link on right
@@ -4135,7 +4783,7 @@ class WidgetRoot {
4135
4783
  totalMembers: joinData.group.max_participants,
4136
4784
  timeLeft: joinData.group.timeLeftSeconds,
4137
4785
  };
4138
- const lobbyModal = new LobbyModal(lobbyData, {}, null, null, null, this.config.debug);
4786
+ const lobbyModal = new LobbyModal(lobbyData, {}, null, null, this.config.debug);
4139
4787
  lobbyModal.open(joinData.group.id);
4140
4788
  });
4141
4789
  // Set callback for viewing progress on already joined group
@@ -4152,7 +4800,7 @@ class WidgetRoot {
4152
4800
  currentMembers: groupData.joined,
4153
4801
  totalMembers: groupData.total,
4154
4802
  };
4155
- const lobbyModal = new LobbyModal(lobbyData, {}, null, null, null, this.config.debug);
4803
+ const lobbyModal = new LobbyModal(lobbyData, {}, null, null, this.config.debug);
4156
4804
  lobbyModal.open(groupId);
4157
4805
  });
4158
4806
  }
@@ -4160,21 +4808,104 @@ class WidgetRoot {
4160
4808
  }
4161
4809
  /**
4162
4810
  * Render the widget into the specified container
4811
+ *
4812
+ * Asynchronous method that renders the collaborative buying widget.
4813
+ * Automatically fetches product data, groups, and set up event listeners.
4814
+ * Includes request deduplication to prevent redundant API calls from rapid re-renders
4815
+ * (e.g., React component re-rendering).
4816
+ *
4817
+ * @param {RenderWidgetOptions} options - Widget rendering options
4818
+ * @returns {Promise<void>} Rejects if rendering fails
4819
+ *
4820
+ * @throws {CoBuyRenderError} If container not found or rendering fails
4821
+ * @throws {CoBuyValidationError} If productId validation fails
4822
+ *
4823
+ * @example
4824
+ * ```typescript
4825
+ * try {
4826
+ * const widget = new WidgetRoot(config, apiClient, analyticsClient);
4827
+ * await widget.render({
4828
+ * productId: 'prod_123',
4829
+ * container: '#widget-container'
4830
+ * });
4831
+ * } catch (error) {
4832
+ * if (error instanceof CoBuyRenderError) {
4833
+ * console.error('Widget render failed:', error.message);
4834
+ * }
4835
+ * }
4836
+ * ```
4163
4837
  */
4164
4838
  async render(options) {
4165
- var _a, _b, _c, _d, _e;
4839
+ // Request deduplication: Skip render if nothing changed
4840
+ const resolvedContainer = this.resolveContainer(options.container);
4841
+ const isDuplicate = this.lastRenderedProductId === options.productId &&
4842
+ this.lastRenderedContainer === resolvedContainer;
4843
+ if (isDuplicate) {
4844
+ this.logger.debug(`[Dedup] Skipping duplicate render for productId=${options.productId}`);
4845
+ return;
4846
+ }
4847
+ // Clear previous debounce timer if exists
4848
+ if (this.renderDebounceTimer) {
4849
+ clearTimeout(this.renderDebounceTimer);
4850
+ this.renderDebounceTimer = null;
4851
+ }
4852
+ // Store pending options and debounce the actual render
4853
+ this.pendingRenderOptions = options;
4854
+ this.renderDebounceTimer = setTimeout(() => {
4855
+ this.renderDebounceTimer = null;
4856
+ if (this.pendingRenderOptions) {
4857
+ const opts = this.pendingRenderOptions;
4858
+ this.pendingRenderOptions = null;
4859
+ void this.doQueuedRender(opts);
4860
+ }
4861
+ }, this.RENDER_DEBOUNCE_MS);
4862
+ }
4863
+ /**
4864
+ * Queued render that handles concurrent render protection
4865
+ */
4866
+ async doQueuedRender(options) {
4867
+ if (this.isRendering) {
4868
+ this.logger.debug("Render already in progress, queuing request");
4869
+ if (this.renderPromise) {
4870
+ await this.renderPromise;
4871
+ }
4872
+ // After current render completes, try again with latest options
4873
+ return this.doQueuedRender(options);
4874
+ }
4875
+ this.isRendering = true;
4876
+ this.renderPromise = this.doRender(options);
4877
+ try {
4878
+ await this.renderPromise;
4879
+ // Update deduplication tracking after successful render
4880
+ const resolvedContainer = this.resolveContainer(options.container);
4881
+ this.lastRenderedProductId = options.productId;
4882
+ this.lastRenderedContainer = resolvedContainer;
4883
+ }
4884
+ finally {
4885
+ this.isRendering = false;
4886
+ this.renderPromise = null;
4887
+ }
4888
+ }
4889
+ async doRender(options) {
4890
+ var _a, _b, _c, _d, _e, _f;
4166
4891
  try {
4167
4892
  // Resolve container
4168
4893
  const container = this.resolveContainer(options.container);
4169
4894
  if (!container) {
4170
4895
  const selector = typeof options.container === "string" ? options.container : "<HTMLElement>";
4171
- const errorMessage = `CoBuy: container not found for ${selector}`;
4896
+ const errorMessage = `Container not found for selector: ${selector}`;
4172
4897
  this.logger.error(errorMessage);
4173
- const error = new Error(errorMessage);
4174
- (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
4175
- throw error;
4898
+ (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, new Error(errorMessage));
4899
+ throw new CoBuyRenderError(errorMessage, { selector });
4176
4900
  }
4177
- // Track current render context
4901
+ // Validate product ID format
4902
+ if (!validateProductId(options.productId)) {
4903
+ const errorMessage = `Invalid productId format. Must be a non-empty string (max 200 chars): ${options.productId}`;
4904
+ this.logger.error(errorMessage);
4905
+ (_b = options.onError) === null || _b === void 0 ? void 0 : _b.call(options, new Error(errorMessage));
4906
+ throw new CoBuyValidationError(errorMessage, { productId: options.productId });
4907
+ }
4908
+ // Track current render context
4178
4909
  this.currentOptions = options;
4179
4910
  this.currentProductId = options.productId;
4180
4911
  this.groupFulfilled = false;
@@ -4185,7 +4916,7 @@ class WidgetRoot {
4185
4916
  // Listen for realtime fulfillment updates once per page
4186
4917
  this.subscribeToSocketEvents();
4187
4918
  // Host safety + idempotency markers
4188
- container.setAttribute("data-cobuy-mounted", "true");
4919
+ container.dataset.cobuyMounted = "true";
4189
4920
  container.style.maxWidth = "100%";
4190
4921
  container.style.width = "100%";
4191
4922
  container.style.boxSizing = "border-box";
@@ -4204,20 +4935,30 @@ class WidgetRoot {
4204
4935
  this.state = WidgetState.LOADING;
4205
4936
  this.retryCount = 0;
4206
4937
  // Emit loading event
4207
- (_c = (_b = this.events) === null || _b === void 0 ? void 0 : _b.onLoading) === null || _c === void 0 ? void 0 : _c.call(_b, options.productId);
4938
+ (_d = (_c = this.events) === null || _c === void 0 ? void 0 : _c.onLoading) === null || _d === void 0 ? void 0 : _d.call(_c, options.productId);
4208
4939
  // Render skeleton loading state for widget
4209
4940
  this.renderSkeleton(container);
4210
4941
  // Render skeleton for group info in separate container if provided
4211
4942
  if (options.groupContainer) {
4212
4943
  const groupContainer = this.resolveContainer(options.groupContainer);
4213
- if (groupContainer) {
4944
+ if (!groupContainer) {
4945
+ const selector = typeof options.groupContainer === "string" ? options.groupContainer : "<HTMLElement>";
4946
+ const errorMessage = `CoBuy: groupContainer not found for ${selector}`;
4947
+ this.logger.warn(errorMessage);
4948
+ }
4949
+ else {
4214
4950
  this.renderGroupSkeleton(groupContainer);
4215
4951
  }
4216
4952
  }
4217
4953
  // Render skeleton for reward text in separate container if provided
4218
4954
  if (options.rewardContainer) {
4219
4955
  const rewardContainer = this.resolveContainer(options.rewardContainer);
4220
- if (rewardContainer) {
4956
+ if (!rewardContainer) {
4957
+ const selector = typeof options.rewardContainer === "string" ? options.rewardContainer : "<HTMLElement>";
4958
+ const errorMessage = `CoBuy: rewardContainer not found for ${selector}`;
4959
+ this.logger.warn(errorMessage);
4960
+ }
4961
+ else {
4221
4962
  this.renderRewardSkeleton(rewardContainer);
4222
4963
  }
4223
4964
  }
@@ -4230,6 +4971,13 @@ class WidgetRoot {
4230
4971
  this.currentGroupData = groupData;
4231
4972
  this.currentGroupId = (groupData === null || groupData === void 0 ? void 0 : groupData.id) || null;
4232
4973
  this.currentRewardData = rewardData;
4974
+ // Explicitly set state based on fetch result
4975
+ if (rewardData) {
4976
+ this.state = WidgetState.LOADED;
4977
+ }
4978
+ else {
4979
+ this.state = WidgetState.ERROR;
4980
+ }
4233
4981
  console.log("group data for product ", options.productId, groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption, isValidOfflineRedemption(groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption));
4234
4982
  // Extract offline_redemption from API response if available
4235
4983
  if ((groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption) &&
@@ -4249,6 +4997,10 @@ class WidgetRoot {
4249
4997
  else {
4250
4998
  this.state = WidgetState.LOADED;
4251
4999
  }
5000
+ if (typeof document !== "undefined" && !document.body.contains(container)) {
5001
+ this.logger.warn("Container removed from DOM, aborting render");
5002
+ return;
5003
+ }
4252
5004
  // Render group info in separate container if provided
4253
5005
  if (options.groupContainer) {
4254
5006
  const groupContainer = this.resolveContainer(options.groupContainer);
@@ -4269,6 +5021,11 @@ class WidgetRoot {
4269
5021
  groupContainer.style.display = "none";
4270
5022
  }
4271
5023
  }
5024
+ else {
5025
+ // Warn if container was specified but not found
5026
+ const selector = typeof options.groupContainer === "string" ? options.groupContainer : "<HTMLElement>";
5027
+ this.logger.warn(`CoBuy: groupContainer not found for ${selector} - skipping group render`);
5028
+ }
4272
5029
  }
4273
5030
  // Render reward text in separate container if provided and we have group data
4274
5031
  if (options.rewardContainer) {
@@ -4285,9 +5042,13 @@ class WidgetRoot {
4285
5042
  rewardContainer.style.display = "none";
4286
5043
  }
4287
5044
  }
5045
+ else {
5046
+ // Warn if container was specified but not found
5047
+ const selector = typeof options.rewardContainer === "string" ? options.rewardContainer : "<HTMLElement>";
5048
+ this.logger.warn(`CoBuy: rewardContainer not found for ${selector} - skipping reward render`);
5049
+ }
4288
5050
  }
4289
5051
  // Render widget or error based on final state
4290
- // Note: state may be ERROR after fetchRewardWithRetry despite TypeScript's flow analysis
4291
5052
  if (this.state === WidgetState.ERROR) {
4292
5053
  this.renderError(container);
4293
5054
  }
@@ -4305,7 +5066,7 @@ class WidgetRoot {
4305
5066
  catch (error) {
4306
5067
  this.logger.error("Failed to render widget", error);
4307
5068
  this.state = WidgetState.ERROR;
4308
- (_e = (_d = this.events) === null || _d === void 0 ? void 0 : _d.onError) === null || _e === void 0 ? void 0 : _e.call(_d, options.productId, error);
5069
+ (_f = (_e = this.events) === null || _e === void 0 ? void 0 : _e.onError) === null || _f === void 0 ? void 0 : _f.call(_e, options.productId, error);
4309
5070
  if (options.onError) {
4310
5071
  options.onError(error);
4311
5072
  }
@@ -4316,12 +5077,15 @@ class WidgetRoot {
4316
5077
  }
4317
5078
  /**
4318
5079
  * Fetch reward data with automatic retry on failure
5080
+ *
5081
+ * @param productId The product ID to fetch reward for
5082
+ * @returns Promise with reward data on success, null on failure
5083
+ * @internal State updates (LOADED/ERROR) are handled by the caller
4319
5084
  */
4320
5085
  async fetchRewardWithRetry(productId) {
4321
5086
  var _a, _b, _c, _d;
4322
5087
  try {
4323
5088
  const reward = await this.fetchReward(productId);
4324
- this.state = WidgetState.LOADED;
4325
5089
  this.logger.info("Reward data fetched successfully");
4326
5090
  (_b = (_a = this.events) === null || _a === void 0 ? void 0 : _a.onLoaded) === null || _b === void 0 ? void 0 : _b.call(_a, productId, reward);
4327
5091
  return reward;
@@ -4330,10 +5094,10 @@ class WidgetRoot {
4330
5094
  if (this.retryCount < this.MAX_RETRIES) {
4331
5095
  this.retryCount++;
4332
5096
  this.logger.info(`Retrying reward fetch (${this.retryCount}/${this.MAX_RETRIES})`, error);
4333
- // Immediate retry
5097
+ // Immediate retry - don't set error state yet
4334
5098
  return this.fetchRewardWithRetry(productId);
4335
5099
  }
4336
- this.state = WidgetState.ERROR;
5100
+ // Final failure - caller will set ERROR state based on null return
4337
5101
  this.logger.error("Failed to fetch reward after retries", error);
4338
5102
  (_d = (_c = this.events) === null || _c === void 0 ? void 0 : _c.onError) === null || _d === void 0 ? void 0 : _d.call(_c, productId, error);
4339
5103
  return null;
@@ -4371,8 +5135,9 @@ class WidgetRoot {
4371
5135
  */
4372
5136
  renderSkeleton(container) {
4373
5137
  // Inject animations once
4374
- const shimmerStyle = document.createElement("style");
4375
- shimmerStyle.textContent = `
5138
+ if (!skeletonStylesInjected && !document.querySelector("#cobuy-shimmer-style")) {
5139
+ const shimmerStyle = document.createElement("style");
5140
+ shimmerStyle.textContent = `
4376
5141
  @keyframes cobuy-shimmer {
4377
5142
  0% { background-position: -200px 0; }
4378
5143
  100% { background-position: calc(200px + 100%) 0; }
@@ -4395,10 +5160,12 @@ class WidgetRoot {
4395
5160
  background-size: 200px 100%;
4396
5161
  }
4397
5162
  `;
4398
- if (!document.querySelector("#cobuy-shimmer-style")) {
4399
5163
  shimmerStyle.id = "cobuy-shimmer-style";
4400
5164
  document.head.appendChild(shimmerStyle);
4401
5165
  }
5166
+ if (!skeletonStylesInjected) {
5167
+ skeletonStylesInjected = true;
5168
+ }
4402
5169
  // Button placeholder only - reward text is rendered in a separate container
4403
5170
  const buttonPlaceholder = document.createElement("div");
4404
5171
  buttonPlaceholder.className = "cobuy-skeleton-button";
@@ -4414,6 +5181,8 @@ class WidgetRoot {
4414
5181
  "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards 100ms";
4415
5182
  container.innerHTML = "";
4416
5183
  container.appendChild(buttonPlaceholder);
5184
+ // Announce to screen readers that content is loading
5185
+ this.announceToScreenReaders("Loading CoBuy offer");
4417
5186
  }
4418
5187
  /**
4419
5188
  * Render skeleton loading state for group info in separate container
@@ -4567,6 +5336,8 @@ class WidgetRoot {
4567
5336
  container.innerHTML = "";
4568
5337
  container.appendChild(message);
4569
5338
  container.appendChild(retryButton);
5339
+ // Announce error to screen readers
5340
+ this.announceToScreenReaders("Error: Unable to load CoBuy offer. Use the retry button to try again.");
4570
5341
  }
4571
5342
  /**
4572
5343
  * Handle retry button click - reset retry count and re-fetch
@@ -4586,12 +5357,9 @@ class WidgetRoot {
4586
5357
  this.renderSkeleton(this.currentContainer);
4587
5358
  const rewardData = await this.fetchRewardWithRetry(this.currentProductId);
4588
5359
  this.currentRewardData = rewardData;
4589
- // Render based on final state (state may be ERROR after fetchRewardWithRetry)
4590
- if (this.state === WidgetState.ERROR) {
4591
- this.renderError(this.currentContainer);
4592
- }
4593
- else {
4594
- // LOADED state
5360
+ // Set state based on fetch result (LOADED if data exists, ERROR otherwise)
5361
+ if (rewardData) {
5362
+ this.state = WidgetState.LOADED;
4595
5363
  // Use default layout when retrying (we don't have the original options here),
4596
5364
  // but preserve existing structure by rendering in the standard order.
4597
5365
  this.createWidget(rewardData, this.currentContainer, {
@@ -4600,6 +5368,46 @@ class WidgetRoot {
4600
5368
  layout: ["group", "reward", "cta"],
4601
5369
  });
4602
5370
  }
5371
+ else {
5372
+ this.state = WidgetState.ERROR;
5373
+ this.renderError(this.currentContainer);
5374
+ }
5375
+ }
5376
+ /**
5377
+ * Get or create a live region announcer for screen reader announcements
5378
+ */
5379
+ getOrCreateLiveRegion() {
5380
+ if (this.liveRegionAnnouncer) {
5381
+ return this.liveRegionAnnouncer;
5382
+ }
5383
+ // Check if one already exists in the DOM
5384
+ const existing = document.querySelector("#cobuy-live-region");
5385
+ if (existing instanceof HTMLDivElement) {
5386
+ this.liveRegionAnnouncer = existing;
5387
+ return this.liveRegionAnnouncer;
5388
+ }
5389
+ // Create new live region
5390
+ const announcer = document.createElement("div");
5391
+ announcer.id = "cobuy-live-region";
5392
+ announcer.setAttribute("aria-live", "polite");
5393
+ announcer.setAttribute("aria-atomic", "true");
5394
+ announcer.className = "sr-only";
5395
+ // Screen reader only styles - hidden from visual viewport
5396
+ announcer.style.position = "absolute";
5397
+ announcer.style.left = "-10000px";
5398
+ announcer.style.width = "1px";
5399
+ announcer.style.height = "1px";
5400
+ announcer.style.overflow = "hidden";
5401
+ document.body.appendChild(announcer);
5402
+ this.liveRegionAnnouncer = announcer;
5403
+ return this.liveRegionAnnouncer;
5404
+ }
5405
+ /**
5406
+ * Announce a message to screen readers via live region
5407
+ */
5408
+ announceToScreenReaders(message) {
5409
+ const region = this.getOrCreateLiveRegion();
5410
+ region.textContent = message;
4603
5411
  }
4604
5412
  createWidget(rewardData, container, options) {
4605
5413
  const isFulfilled = this.groupFulfilled;
@@ -4644,35 +5452,11 @@ class WidgetRoot {
4644
5452
  rewardLine.style.opacity = "0";
4645
5453
  rewardLine.style.animation =
4646
5454
  "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards";
4647
- // if (isFulfilled) {
4648
- // const rewardText = this.formatRewardText(activeReward);
4649
- // rewardLine.textContent = rewardText
4650
- // ? `Reward available: ${rewardText}`
4651
- // : "Reward available for this group";
4652
- // rewardLine.setAttribute(
4653
- // "aria-label",
4654
- // rewardText ? `Reward locked in: ${rewardText}` : "Reward available for this group",
4655
- // );
4656
- // rewardLine.title = rewardLine.textContent;
4657
- // } else if (activeReward) {
4658
- // const rewardText = this.formatRewardText(activeReward);
4659
- // rewardLine.textContent = rewardText
4660
- // ? `Save up to ${rewardText} with CoBuy`
4661
- // : "CoBuy reward available";
4662
- // rewardLine.setAttribute("aria-label", `Eligible for CoBuy reward: ${rewardLine.textContent}`);
4663
- // rewardLine.title = rewardLine.textContent;
4664
- // } else {
4665
- // rewardLine.textContent = "CoBuy offer loading or unavailable";
4666
- // rewardLine.setAttribute("aria-label", "CoBuy offer loading or unavailable");
4667
- // rewardLine.title = "CoBuy offer loading or unavailable";
4668
- // rewardLine.style.color = "#6b7280";
4669
- // }
4670
5455
  sections.reward = rewardLine;
4671
5456
  // Button - semantic button element with accessibility
4672
5457
  const button = document.createElement("button");
4673
5458
  button.type = "button";
4674
5459
  button.className = "cobuy-button";
4675
- button.textContent = isFulfilled ? "Continue to Checkout" : "CoBuy";
4676
5460
  button.setAttribute("aria-label", isFulfilled
4677
5461
  ? "Continue to checkout for this fulfilled group"
4678
5462
  : "Buy this product with CoBuy for group buying discounts");
@@ -4681,20 +5465,20 @@ class WidgetRoot {
4681
5465
  button.title = isFulfilled
4682
5466
  ? "Continue to checkout"
4683
5467
  : "Buy with CoBuy for discounts when others join";
4684
- button.style.padding = "10px 14px";
5468
+ button.style.padding = "8px 12px";
4685
5469
  button.style.backgroundColor = isFulfilled
4686
5470
  ? "var(--cobuy-success-color, #10b981)"
4687
5471
  : "var(--cobuy-primary-color, #4f46e5)";
4688
5472
  button.style.color = "var(--cobuy-button-text-color, white)";
4689
5473
  button.style.border = "none";
4690
5474
  button.style.borderRadius = "var(--cobuy-border-radius, 6px)";
4691
- button.style.fontSize = "13px";
5475
+ button.style.fontSize = "12px";
4692
5476
  button.style.fontWeight = "700";
4693
5477
  button.style.fontFamily =
4694
5478
  "var(--cobuy-font-family, Inter, system-ui, -apple-system, sans-serif)";
4695
5479
  button.style.cursor = "pointer";
4696
5480
  button.style.width = "100%";
4697
- button.style.minHeight = "var(--cobuy-cta-min-height, 44px)";
5481
+ button.style.minHeight = "var(--cobuy-cta-min-height, 36px)";
4698
5482
  button.style.height = "auto";
4699
5483
  button.style.lineHeight = "1.4";
4700
5484
  button.style.wordWrap = "break-word";
@@ -4703,6 +5487,41 @@ class WidgetRoot {
4703
5487
  button.style.opacity = "0";
4704
5488
  button.style.animation =
4705
5489
  "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards 100ms";
5490
+ button.style.display = "flex";
5491
+ button.style.alignItems = "center";
5492
+ button.style.justifyContent = "center";
5493
+ // Add logo or text based on state
5494
+ if (isFulfilled) {
5495
+ button.textContent = "Continue to Checkout";
5496
+ }
5497
+ else {
5498
+ // Create SVG logo for CoBuy button
5499
+ const svgNS = "http://www.w3.org/2000/svg";
5500
+ const svg = document.createElementNS(svgNS, "svg");
5501
+ svg.setAttribute("width", "81");
5502
+ svg.setAttribute("height", "20");
5503
+ svg.setAttribute("viewBox", "0 0 81 20");
5504
+ svg.setAttribute("fill", "none");
5505
+ svg.style.height = "auto";
5506
+ svg.style.maxHeight = "18px";
5507
+ svg.style.width = "auto";
5508
+ // CoBuy text path
5509
+ const textPath = document.createElementNS(svgNS, "path");
5510
+ textPath.setAttribute("d", "M34.752 16.192C33.8987 16.192 33.1093 16.0373 32.384 15.728C31.6693 15.4187 31.04 14.9867 30.496 14.432C29.9627 13.8773 29.5467 13.2267 29.248 12.48C28.9493 11.7333 28.8 10.9173 28.8 10.032C28.8 9.14667 28.944 8.33067 29.232 7.584C29.5307 6.82667 29.9467 6.176 30.48 5.632C31.024 5.07733 31.6587 4.65067 32.384 4.352C33.1093 4.04267 33.8987 3.888 34.752 3.888C35.6053 3.888 36.368 4.032 37.04 4.32C37.7227 4.608 38.2987 4.992 38.768 5.472C39.2373 5.94133 39.5733 6.45867 39.776 7.024L37.824 7.936C37.6 7.33867 37.2213 6.848 36.688 6.464C36.1547 6.06933 35.5093 5.872 34.752 5.872C34.0053 5.872 33.344 6.048 32.768 6.4C32.2027 6.752 31.76 7.23733 31.44 7.856C31.1307 8.47467 30.976 9.2 30.976 10.032C30.976 10.864 31.1307 11.5947 31.44 12.224C31.76 12.8427 32.2027 13.328 32.768 13.68C33.344 14.032 34.0053 14.208 34.752 14.208C35.5093 14.208 36.1547 14.016 36.688 13.632C37.2213 13.2373 37.6 12.7413 37.824 12.144L39.776 13.056C39.5733 13.6213 39.2373 14.144 38.768 14.624C38.2987 15.0933 37.7227 15.472 37.04 15.76C36.368 16.048 35.6053 16.192 34.752 16.192ZM45.4664 16.192C44.613 16.192 43.8344 15.9947 43.1304 15.6C42.437 15.2053 41.8824 14.6667 41.4664 13.984C41.061 13.3013 40.8584 12.5227 40.8584 11.648C40.8584 10.7733 41.061 9.99467 41.4664 9.312C41.8824 8.62933 42.437 8.09067 43.1304 7.696C43.8237 7.30133 44.6024 7.104 45.4664 7.104C46.3197 7.104 47.093 7.30133 47.7864 7.696C48.4797 8.09067 49.029 8.62933 49.4344 9.312C49.8504 9.984 50.0584 10.7627 50.0584 11.648C50.0584 12.5227 49.8504 13.3013 49.4344 13.984C49.0184 14.6667 48.4637 15.2053 47.7704 15.6C47.077 15.9947 46.309 16.192 45.4664 16.192ZM45.4664 14.272C45.9357 14.272 46.3464 14.16 46.6984 13.936C47.061 13.712 47.3437 13.4027 47.5464 13.008C47.7597 12.6027 47.8664 12.1493 47.8664 11.648C47.8664 11.136 47.7597 10.688 47.5464 10.304C47.3437 9.90933 47.061 9.6 46.6984 9.376C46.3464 9.14133 45.9357 9.024 45.4664 9.024C44.9864 9.024 44.565 9.14133 44.2024 9.376C43.8397 9.6 43.5517 9.90933 43.3384 10.304C43.1357 10.688 43.0344 11.136 43.0344 11.648C43.0344 12.1493 43.1357 12.6027 43.3384 13.008C43.5517 13.4027 43.8397 13.712 44.2024 13.936C44.565 14.16 44.9864 14.272 45.4664 14.272ZM51.8239 16V4.08H56.8479C57.6372 4.08 58.3092 4.21867 58.8639 4.496C59.4292 4.76267 59.8612 5.14667 60.1599 5.648C60.4692 6.13867 60.6239 6.736 60.6239 7.44C60.6239 7.984 60.4745 8.496 60.1759 8.976C59.8879 9.44533 59.4239 9.83467 58.7839 10.144V9.136C59.3705 9.36 59.8345 9.63733 60.1759 9.968C60.5172 10.2987 60.7572 10.6667 60.8959 11.072C61.0345 11.4773 61.1039 11.904 61.1039 12.352C61.1039 13.4933 60.7252 14.3893 59.9679 15.04C59.2212 15.68 58.1812 16 56.8479 16H51.8239ZM53.9999 14.08H57.0719C57.6372 14.08 58.0852 13.9253 58.4159 13.616C58.7572 13.296 58.9279 12.8747 58.9279 12.352C58.9279 11.8293 58.7572 11.408 58.4159 11.088C58.0852 10.768 57.6372 10.608 57.0719 10.608H53.9999V14.08ZM53.9999 8.704H56.9599C57.4079 8.704 57.7652 8.576 58.0319 8.32C58.2985 8.05333 58.4319 7.712 58.4319 7.296C58.4319 6.88 58.2985 6.54933 58.0319 6.304C57.7652 6.05867 57.4079 5.936 56.9599 5.936H53.9999V8.704ZM65.8144 16.192C65.1424 16.192 64.5557 16.0427 64.0544 15.744C63.5637 15.4453 63.185 15.0293 62.9184 14.496C62.6624 13.9627 62.5344 13.3387 62.5344 12.624V7.296H64.6304V12.448C64.6304 12.8107 64.6997 13.1307 64.8384 13.408C64.9877 13.6747 65.1957 13.888 65.4624 14.048C65.7397 14.1973 66.049 14.272 66.3904 14.272C66.7317 14.272 67.0357 14.1973 67.3024 14.048C67.569 13.888 67.777 13.6693 67.9264 13.392C68.0757 13.1147 68.1504 12.784 68.1504 12.4V7.296H70.2464V16H68.2624V14.288L68.4384 14.592C68.2357 15.1253 67.8997 15.5253 67.4304 15.792C66.9717 16.0587 66.433 16.192 65.8144 16.192ZM73.2508 19.52C73.0161 19.52 72.7868 19.4987 72.5628 19.456C72.3388 19.424 72.1361 19.36 71.9548 19.264V17.52C72.0934 17.552 72.2588 17.584 72.4508 17.616C72.6534 17.648 72.8401 17.664 73.0108 17.664C73.4908 17.664 73.8374 17.552 74.0508 17.328C74.2748 17.1147 74.4721 16.8267 74.6428 16.464L75.2188 15.12L75.1868 16.88L71.3948 7.296H73.6508L76.3068 14.368H75.5068L78.1468 7.296H80.4188L76.6268 16.88C76.4028 17.4347 76.1308 17.9093 75.8108 18.304C75.4907 18.6987 75.1174 18.9973 74.6908 19.2C74.2748 19.4133 73.7948 19.52 73.2508 19.52Z");
5511
+ textPath.setAttribute("fill", "white");
5512
+ // Orange square background
5513
+ const rect = document.createElementNS(svgNS, "path");
5514
+ rect.setAttribute("d", "M1.51641 1.76251H19.5066V17.964H1.51641V1.76251Z");
5515
+ rect.setAttribute("fill", "#FF8A15");
5516
+ // Blue accent
5517
+ const accent = document.createElementNS(svgNS, "path");
5518
+ accent.setAttribute("d", "M2.37111 0.142428C1.40613 0.476852 0.735229 1.10051 0.25733 2.11282L0 2.64609V17.2071L0.349234 17.894C0.75361 18.6985 1.41532 19.3492 2.23326 19.7469L2.75711 20H18.1969L18.7024 19.765C19.4284 19.4306 20.081 18.8792 20.421 18.3188C21.0092 17.3517 21 17.4783 21 9.7865C21 2.20321 21.0092 2.31167 20.4853 1.4982C20.1453 0.964931 19.5663 0.440698 19.0884 0.223774L18.6564 0.0249275L10.7527 0.0068505C3.34529 -0.0112265 2.82144 -0.00218799 2.37111 0.142428ZM9.93479 3.41436C10.3575 3.59513 10.9825 4.03802 11.2858 4.36341C11.3777 4.46283 11.5891 4.80629 11.7453 5.13168C12.021 5.69206 12.0394 5.77341 12.0394 6.75861C12.0394 7.68053 12.0118 7.85226 11.8096 8.24996C11.5155 8.85554 10.7895 9.54246 10.0726 9.89496C9.52122 10.1752 9.42932 10.1932 8.54704 10.1932C7.66477 10.1932 7.57286 10.1752 7.03063 9.89496C5.8175 9.28938 5.12822 8.43073 4.87089 7.20149C4.70547 6.44226 4.7698 6.01745 5.16499 5.13168C5.54179 4.28206 6.65383 3.45052 7.70153 3.23359C8.28971 3.10705 9.45689 3.19744 9.93479 3.41436ZM15.4306 9.73227C16.0556 10.0396 16.5519 10.5548 16.8827 11.2417C17.1217 11.7569 17.1584 11.9015 17.1217 12.4348C17.0481 13.6098 16.414 14.5679 15.3755 15.0288C14.6219 15.3633 13.8407 15.4175 13.0963 15.1644C12.3794 14.9294 12.0026 14.6582 11.6258 14.125C10.7619 12.8957 10.8722 11.4586 11.9107 10.3469C12.5081 9.71419 13.179 9.46112 14.1991 9.48823C14.8057 9.50631 15.0814 9.56958 15.4306 9.73227ZM7.36148 11.558C7.93129 11.7027 8.64814 12.3534 8.94223 12.9952C9.11684 13.3657 9.17199 13.655 9.18118 14.125C9.19037 15.6886 7.98643 16.7823 6.34135 16.6829C5.59693 16.6377 5.21094 16.466 4.65952 15.9508C3.75886 15.0921 3.63939 13.4832 4.41138 12.498C5.04551 11.6755 6.25864 11.2869 7.36148 11.558Z");
5519
+ accent.setAttribute("fill", "#0157AA");
5520
+ svg.appendChild(rect);
5521
+ svg.appendChild(accent);
5522
+ svg.appendChild(textPath);
5523
+ button.appendChild(svg);
5524
+ }
4706
5525
  // Hover effect using opacity for better theme compatibility
4707
5526
  button.addEventListener("mouseenter", () => {
4708
5527
  button.style.opacity = "0.9";
@@ -4736,6 +5555,10 @@ class WidgetRoot {
4736
5555
  });
4737
5556
  container.innerHTML = "";
4738
5557
  container.appendChild(wrapper);
5558
+ // Announce loaded widget to screen readers
5559
+ const rewardText = rewardLine.textContent || "CoBuy offer";
5560
+ const buttonText = button.getAttribute("aria-label") || "CoBuy action";
5561
+ this.announceToScreenReaders(`${rewardText}. ${buttonText}`);
4739
5562
  }
4740
5563
  /**
4741
5564
  * Inject base layout styles for responsive widget rendering
@@ -4747,6 +5570,19 @@ class WidgetRoot {
4747
5570
  const style = document.createElement("style");
4748
5571
  style.id = "cobuy-widget-base-styles";
4749
5572
  style.textContent = `
5573
+ /* Screen reader only - hide visually but keep available to AT */
5574
+ .sr-only {
5575
+ position: absolute;
5576
+ width: 1px;
5577
+ height: 1px;
5578
+ padding: 0;
5579
+ margin: -1px;
5580
+ overflow: hidden;
5581
+ clip: rect(0, 0, 0, 0);
5582
+ white-space: nowrap;
5583
+ border-width: 0;
5584
+ }
5585
+
4750
5586
  .cobuy-widget {
4751
5587
  display: grid;
4752
5588
  grid-template-columns: 1fr;
@@ -5167,12 +6003,35 @@ function buildApiUrl(baseUrl, endpoint, params, query) {
5167
6003
  */
5168
6004
  class ApiClient {
5169
6005
  constructor(config) {
6006
+ var _a;
5170
6007
  this.traceId = null;
6008
+ this.rewardCache = new Map();
6009
+ this.REWARD_CACHE_TTL = 60000; // 1 minute
6010
+ this.pendingRequests = new Map();
5171
6011
  this.baseUrl = config.baseUrl;
5172
6012
  this.authStrategy = config.authStrategy;
5173
6013
  this.sessionId = config.sessionId;
5174
6014
  this.logger = new Logger(config.debug || false);
5175
6015
  this.defaultTimeout = config.timeout || 30000; // 30 seconds default
6016
+ this.maxRetries = (_a = config.maxRetries) !== null && _a !== void 0 ? _a : 2; // Default: 2 retries
6017
+ this.botdScore = typeof config.botdScore === "number" ? config.botdScore : undefined;
6018
+ }
6019
+ /**
6020
+ * Update the BotD (Bot Detection) score for fraud detection
6021
+ *
6022
+ * @param {number | undefined} score - The BotD score (0-100) or undefined to clear
6023
+ * @returns {void}
6024
+ *
6025
+ * @description
6026
+ * Sets the fraud detection score that will be included in API request headers.
6027
+ * Used to improve fraud detection accuracy. Cleared if undefined is passed.
6028
+ *
6029
+ * @example
6030
+ * CoBuy.getApiClient()?.setBotdScore(45); // Set BotD score
6031
+ * CoBuy.getApiClient()?.setBotdScore(undefined); // Clear score
6032
+ */
6033
+ setBotdScore(score) {
6034
+ this.botdScore = typeof score === "number" ? score : undefined;
5176
6035
  }
5177
6036
  /**
5178
6037
  * Generate a unique trace ID for request correlation
@@ -5195,13 +6054,51 @@ class ApiClient {
5195
6054
  error.statusCode === 504);
5196
6055
  }
5197
6056
  /**
5198
- * Make an HTTP request to the API with retry logic
6057
+ * Make an authenticated HTTP request with automatic retry logic
6058
+ *
6059
+ * Low-level method for making requests to the API. Handles authentication headers,
6060
+ * timeout management, error handling, and automatic retries for transient failures.
6061
+ *
6062
+ * @template T - The type of data returned on success
6063
+ * @param {string} endpoint - The API endpoint path (e.g., '/products/12345/reward')
6064
+ * @param {ApiRequestOptions} [options={}] - Request configuration options
6065
+ * @param {"GET" | "POST" | "PUT" | "DELETE"} [options.method="GET"] - HTTP method
6066
+ * @param {unknown} [options.body] - Request body (automatically JSON stringified)
6067
+ * @param {Record<string, string>} [options.headers] - Additional headers to include
6068
+ * @param {number} [options.timeout] - Request timeout in milliseconds
6069
+ *
6070
+ * @returns {Promise<ApiResponse<T>>} Response with success flag and data or error
6071
+ *
6072
+ * @description
6073
+ * Request Features:
6074
+ * - Automatic authentication header injection from auth strategy
6075
+ * - Session tracking via X-CoBuy-Session header
6076
+ * - Request correlation via trace ID (X-CoBuy-Trace header)
6077
+ * - Configurable timeout (default 30 seconds)
6078
+ * - Automatic retry on transient errors (timeout, 502, 503, 504)
6079
+ * - Exponential backoff: 100ms, 200ms, 400ms
6080
+ * - Auth token refresh on 401/403 responses
6081
+ * - Full response body logging in debug mode
6082
+ *
6083
+ * @example
6084
+ * ```typescript
6085
+ * const client = CoBuy.getApiClient();
6086
+ * const response = await client.request('/products/abc123/groups', {
6087
+ * method: 'GET',
6088
+ * timeout: 15000
6089
+ * });
6090
+ *
6091
+ * if (response.success) {
6092
+ * console.log('Groups:', response.data);
6093
+ * } else {
6094
+ * console.error('Error:', response.error?.message);
6095
+ * }
6096
+ * ```
5199
6097
  */
5200
6098
  async request(endpoint, options = {}) {
5201
6099
  var _a, _b;
5202
- const maxRetries = 2;
5203
6100
  let lastError = null;
5204
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
6101
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
5205
6102
  const result = await this.executeRequest(endpoint, options);
5206
6103
  // Success - return immediately
5207
6104
  if (result.success) {
@@ -5215,9 +6112,9 @@ class ApiClient {
5215
6112
  if (this.isRetryableError(result.error || {})) {
5216
6113
  lastError = result;
5217
6114
  // Don't retry on last attempt
5218
- if (attempt < maxRetries) {
6115
+ if (attempt < this.maxRetries) {
5219
6116
  const delay = Math.pow(2, attempt) * 100; // 100ms, 200ms, 400ms
5220
- this.logger.info(`Retrying request (attempt ${attempt + 1}/${maxRetries}) after ${delay}ms`);
6117
+ this.logger.info(`Retrying request (attempt ${attempt + 1}/${this.maxRetries}) after ${delay}ms`);
5221
6118
  await new Promise((resolve) => setTimeout(resolve, delay));
5222
6119
  continue;
5223
6120
  }
@@ -5237,21 +6134,33 @@ class ApiClient {
5237
6134
  const url = `${this.baseUrl}${endpoint}`;
5238
6135
  const method = options.method || "GET";
5239
6136
  const timeout = options.timeout || this.defaultTimeout;
6137
+ if (typeof navigator !== "undefined" && navigator.onLine === false) {
6138
+ this.logger.warn(`Offline detected: ${method} ${url}`);
6139
+ return {
6140
+ success: false,
6141
+ error: {
6142
+ message: "No network connection. Check your internet connection and try again.",
6143
+ code: "OFFLINE",
6144
+ },
6145
+ };
6146
+ }
5240
6147
  // Generate trace ID for this request
5241
6148
  this.traceId = this.generateTraceId();
5242
6149
  this.logger.info(`API Request: ${method} ${url} [trace: ${this.traceId}]`);
6150
+ let timeoutId = null;
6151
+ const controller = new AbortController();
5243
6152
  try {
5244
6153
  // Get auth headers from strategy
5245
6154
  const authHeaders = this.authStrategy.getHeaders();
5246
- const controller = new AbortController();
5247
- const timeoutId = setTimeout(() => controller.abort(), timeout);
6155
+ timeoutId = setTimeout(() => controller.abort(), timeout);
5248
6156
  const response = await fetch(url, {
5249
6157
  method,
5250
- headers: Object.assign(Object.assign({ "Content-Type": "application/json", "X-CoBuy-SDK-Version": "1.0.0", "X-CoBuy-Session": this.sessionId, "X-CoBuy-Trace": this.traceId }, authHeaders), options.headers),
6158
+ headers: Object.assign(Object.assign(Object.assign({ "Content-Type": "application/json", "X-CoBuy-SDK-Version": "1.0.0", "X-CoBuy-Session": this.sessionId, "X-CoBuy-Trace": this.traceId }, (this.botdScore !== undefined
6159
+ ? { "X-CoBuy-BotD-Score": this.botdScore.toString() }
6160
+ : {})), authHeaders), options.headers),
5251
6161
  body: options.body ? JSON.stringify(options.body) : undefined,
5252
6162
  signal: controller.signal,
5253
6163
  });
5254
- clearTimeout(timeoutId);
5255
6164
  // Log server session header if present
5256
6165
  const serverSession = response.headers.get("X-CoBuy-Server-Session");
5257
6166
  if (serverSession) {
@@ -5263,9 +6172,55 @@ class ApiClient {
5263
6172
  if (rateLimit && rateLimitRemaining) {
5264
6173
  this.logger.debug(`Rate limit: ${rateLimitRemaining}/${rateLimit}`);
5265
6174
  }
5266
- const responseData = await response.json();
6175
+ let responseData = null;
6176
+ const contentType = response.headers.get("content-type") || "";
6177
+ const responseText = await response.text();
6178
+ if (contentType.includes("application/json")) {
6179
+ if (responseText) {
6180
+ try {
6181
+ responseData = JSON.parse(responseText);
6182
+ }
6183
+ catch (parseError) {
6184
+ this.logger.error("JSON parse error", parseError);
6185
+ return {
6186
+ success: false,
6187
+ error: {
6188
+ message: "Failed to parse API response",
6189
+ code: "PARSE_ERROR",
6190
+ statusCode: response.status,
6191
+ },
6192
+ };
6193
+ }
6194
+ }
6195
+ }
6196
+ else if (responseText) {
6197
+ this.logger.warn(`Non-JSON response received: ${responseText.substring(0, 100)}`);
6198
+ return {
6199
+ success: false,
6200
+ error: {
6201
+ message: response.statusText || "Invalid response format",
6202
+ code: "INVALID_RESPONSE",
6203
+ statusCode: response.status,
6204
+ },
6205
+ };
6206
+ }
5267
6207
  if (!response.ok) {
5268
6208
  this.logger.error(`API Error: ${response.status} ${response.statusText}`, responseData);
6209
+ const responsePayload = responseData && typeof responseData === "object"
6210
+ ? responseData
6211
+ : {};
6212
+ const responseCode = typeof responsePayload.code === "string" ? responsePayload.code : undefined;
6213
+ const responseMessage = typeof responsePayload.message === "string" ? responsePayload.message : undefined;
6214
+ if (response.status === 403 && responseCode === "FRAUD_DETECTED") {
6215
+ return {
6216
+ success: false,
6217
+ error: {
6218
+ message: responseMessage || response.statusText,
6219
+ code: responseCode,
6220
+ statusCode: response.status,
6221
+ },
6222
+ };
6223
+ }
5269
6224
  // Handle auth errors (401/403) with refresh attempt
5270
6225
  if (response.status === 401 || response.status === 403) {
5271
6226
  this.logger.warn("Auth error detected, attempting refresh");
@@ -5277,15 +6232,15 @@ class ApiClient {
5277
6232
  }
5278
6233
  else {
5279
6234
  // Refresh failed, notify error handler
5280
- const authError = new Error(responseData.message || `Authentication failed: ${response.statusText}`);
6235
+ const authError = new Error(responseMessage || `Authentication failed: ${response.statusText}`);
5281
6236
  this.authStrategy.onAuthError(authError);
5282
6237
  }
5283
6238
  }
5284
6239
  return {
5285
6240
  success: false,
5286
6241
  error: {
5287
- message: responseData.message || response.statusText,
5288
- code: responseData.code,
6242
+ message: responseMessage || response.statusText,
6243
+ code: responseCode,
5289
6244
  statusCode: response.status,
5290
6245
  },
5291
6246
  };
@@ -5308,6 +6263,22 @@ class ApiClient {
5308
6263
  },
5309
6264
  };
5310
6265
  }
6266
+ // Detect CORS errors - TypeError with "Failed to fetch" is typically a CORS issue
6267
+ if (error instanceof TypeError &&
6268
+ (error.message.includes("Failed to fetch") ||
6269
+ error.message.includes("CORS") ||
6270
+ error.message.includes("cors"))) {
6271
+ this.logger.error(`CORS Error: ${method} ${url}`, error);
6272
+ return {
6273
+ success: false,
6274
+ error: {
6275
+ message: "CORS policy prevented the request. Verify the API server is configured correctly and allows requests from this origin. " +
6276
+ "Troubleshooting: 1) Check API_BASE_URL is set to a valid endpoint, 2) Ensure server has Access-Control-Allow-Origin headers enabled, " +
6277
+ "3) Confirm the request method is in Access-Control-Allow-Methods",
6278
+ code: "CORS_ERROR",
6279
+ },
6280
+ };
6281
+ }
5311
6282
  this.logger.error(`API Error: ${method} ${url}`, error);
5312
6283
  return {
5313
6284
  success: false,
@@ -5325,6 +6296,11 @@ class ApiClient {
5325
6296
  },
5326
6297
  };
5327
6298
  }
6299
+ finally {
6300
+ if (timeoutId) {
6301
+ clearTimeout(timeoutId);
6302
+ }
6303
+ }
5328
6304
  }
5329
6305
  /**
5330
6306
  * Make a GET request
@@ -5357,14 +6333,30 @@ class ApiClient {
5357
6333
  return this.request(endpoint, Object.assign(Object.assign({}, options), { method: "DELETE" }));
5358
6334
  }
5359
6335
  /**
5360
- * Update the base URL
6336
+ * Update the API base URL
6337
+ *
6338
+ * @param {string} baseUrl - The new base URL for API requests
6339
+ * @returns {void}
6340
+ *
6341
+ * @example
6342
+ * CoBuy.getApiClient()?.setBaseUrl('https://api-staging.cobuy.com');
5361
6343
  */
5362
6344
  setBaseUrl(baseUrl) {
5363
6345
  this.baseUrl = baseUrl;
5364
6346
  this.logger.info(`API Base URL updated: ${baseUrl}`);
5365
6347
  }
5366
6348
  /**
5367
- * Update the auth strategy
6349
+ * Update the authentication strategy
6350
+ *
6351
+ * @param {AuthStrategy} authStrategy - The new auth strategy to use
6352
+ * @returns {void}
6353
+ *
6354
+ * @description
6355
+ * Allows switching between public key and token-based authentication at runtime.
6356
+ *
6357
+ * @example
6358
+ * const newAuth = new TokenAuth("new_token");
6359
+ * CoBuy.getApiClient()?.setAuthStrategy(newAuth);
5368
6360
  */
5369
6361
  setAuthStrategy(authStrategy) {
5370
6362
  this.authStrategy = authStrategy;
@@ -5372,19 +6364,41 @@ class ApiClient {
5372
6364
  }
5373
6365
  /**
5374
6366
  * Get the current session ID
6367
+ *
6368
+ * @returns {string} The current session ID used for API requests
6369
+ *
6370
+ * @example
6371
+ * const sessionId = CoBuy.getApiClient()?.getSessionId();
6372
+ * console.log('Current session:', sessionId);
5375
6373
  */
5376
6374
  getSessionId() {
5377
6375
  return this.sessionId;
5378
6376
  }
5379
6377
  /**
5380
6378
  * Update the session ID
6379
+ *
6380
+ * @param {string} sessionId - The new session ID for API requests
6381
+ * @returns {void}
6382
+ *
6383
+ * @example
6384
+ * CoBuy.getApiClient()?.setSessionId('new-session-uuid');
5381
6385
  */
5382
6386
  setSessionId(sessionId) {
5383
6387
  this.sessionId = sessionId;
5384
6388
  this.logger.info(`Session ID updated: ${sessionId}`);
5385
6389
  }
5386
6390
  /**
5387
- * Get the current trace ID
6391
+ * Get the most recent request trace ID
6392
+ *
6393
+ * Useful for debugging and correlating client requests with server logs.
6394
+ *
6395
+ * @returns {string | null} The trace ID from the last request, or null if no requests made
6396
+ *
6397
+ * @example
6398
+ * const traceId = CoBuy.getApiClient()?.getTraceId();
6399
+ * if (traceId) {
6400
+ * console.log('Last request trace:', traceId); // Use for support tickets
6401
+ * }
5388
6402
  */
5389
6403
  getTraceId() {
5390
6404
  return this.traceId;
@@ -5392,25 +6406,91 @@ class ApiClient {
5392
6406
  /**
5393
6407
  * Get product reward information
5394
6408
  *
5395
- * Retrieves reward data and eligibility for a specific product.
5396
- * This is a core method for SCRUM-247: Retrieve reward information.
6409
+ * Retrieves discount and reward eligibility for a specific product.
6410
+ * Results are cached for 1 minute to reduce API load.
6411
+ *
6412
+ * @param {string} productId - The product ID to fetch reward information for
6413
+ * @returns {Promise<ApiResponse<ProductRewardData>>} Product reward and eligibility data
6414
+ *
6415
+ * @description
6416
+ * Features:
6417
+ * - Caches results for 60 seconds to reduce API calls
6418
+ * - Deduplicates concurrent requests for the same product
6419
+ * - Includes reward amount, eligibility status, and discount percentage
5397
6420
  *
5398
- * @param productId - The product ID to fetch reward information for
5399
- * @returns Promise with reward data including eligibility status
6421
+ * @throws Does not throw; returns ApiResponse with error details on failure
6422
+ *
6423
+ * @example
6424
+ * ```typescript
6425
+ * const response = await client.getProductReward('prod_abc123');
6426
+ * if (response.success) {
6427
+ * console.log('Discount:', response.data?.discount_percentage);
6428
+ * console.log('Eligible:', response.data?.eligible);
6429
+ * }
6430
+ * ```
5400
6431
  */
5401
6432
  async getProductReward(productId) {
6433
+ if (!validateProductId(productId)) {
6434
+ return {
6435
+ success: false,
6436
+ error: {
6437
+ message: "Invalid productId format. Must be a non-empty string (max 200 chars).",
6438
+ code: "INVALID_PRODUCT_ID",
6439
+ },
6440
+ };
6441
+ }
6442
+ const cached = this.rewardCache.get(productId);
6443
+ if (cached && cached.expires > Date.now()) {
6444
+ this.logger.info(`Using cached reward for product: ${productId}`);
6445
+ return {
6446
+ success: true,
6447
+ data: cached.data,
6448
+ };
6449
+ }
6450
+ const cacheKey = `reward:${productId}`;
6451
+ const pending = this.pendingRequests.get(cacheKey);
6452
+ if (pending) {
6453
+ this.logger.info(`Deduplicating request for product: ${productId}`);
6454
+ return pending;
6455
+ }
6456
+ const promise = this.fetchProductReward(productId).then((response) => {
6457
+ if (response.success && response.data) {
6458
+ this.rewardCache.set(productId, {
6459
+ data: response.data,
6460
+ expires: Date.now() + this.REWARD_CACHE_TTL,
6461
+ });
6462
+ }
6463
+ return response;
6464
+ });
6465
+ this.pendingRequests.set(cacheKey, promise);
6466
+ promise.then(() => this.pendingRequests.delete(cacheKey), () => this.pendingRequests.delete(cacheKey));
6467
+ return promise;
6468
+ }
6469
+ /**
6470
+ * Clear all cached product rewards
6471
+ *
6472
+ * Primarily used for testing or forcing fresh data from the server.
6473
+ *
6474
+ * @returns {void}
6475
+ *
6476
+ * @example
6477
+ * CoBuy.getApiClient()?.clearRewardCache();
6478
+ * // Next getProductReward call will fetch fresh data
6479
+ */
6480
+ clearRewardCache() {
6481
+ this.rewardCache.clear();
6482
+ }
6483
+ async fetchProductReward(productId) {
5402
6484
  var _a;
5403
6485
  const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_REWARD, { productId });
5404
6486
  this.logger.info(`Fetching reward for product: ${productId}`);
5405
6487
  const response = await this.get(endpoint);
5406
- // Handle the nested response structure from backend
5407
6488
  if (response.success && ((_a = response.data) === null || _a === void 0 ? void 0 : _a.data)) {
5408
6489
  return {
5409
6490
  success: true,
5410
6491
  data: response.data.data,
5411
6492
  };
5412
6493
  }
5413
- // Return error response
5414
6494
  return {
5415
6495
  success: false,
5416
6496
  error: response.error || {
@@ -5420,10 +6500,42 @@ class ApiClient {
5420
6500
  };
5421
6501
  }
5422
6502
  /**
5423
- * Get primary group information for a product (participants, max, expiry)
6503
+ * Get primary group information for a product
6504
+ *
6505
+ * Retrieves the primary group data including member count, capacity, and expiry time.
6506
+ * Optionally creates a new group if none exists.
6507
+ *
6508
+ * @param {string} productId - The product ID to fetch primary group for
6509
+ * @returns {Promise<ApiResponse<ProductPrimaryGroupData>>} Primary group data or error
6510
+ *
6511
+ * @description
6512
+ * Returns group information including:
6513
+ * - Current participant count
6514
+ * - Maximum participants allowed
6515
+ * - Group status (pending, active, fulfilled, expired)
6516
+ * - Time remaining for group to close
6517
+ * - Group ID for joining operations
6518
+ *
6519
+ * @example
6520
+ * ```typescript
6521
+ * const response = await client.getProductPrimaryGroup('prod_abc123');
6522
+ * if (response.success) {
6523
+ * const group = response.data;
6524
+ * console.log(`${group.participants_count}/${group.max_participants} members`);
6525
+ * }
6526
+ * ```
5424
6527
  */
5425
6528
  async getProductPrimaryGroup(productId) {
5426
6529
  var _a;
6530
+ if (!validateProductId(productId)) {
6531
+ return {
6532
+ success: false,
6533
+ error: {
6534
+ message: "Invalid productId format. Must be a non-empty string (max 200 chars).",
6535
+ code: "INVALID_PRODUCT_ID",
6536
+ },
6537
+ };
6538
+ }
5427
6539
  const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_PRIMARY_GROUP, { productId }, { allowAutoCreate: true });
5428
6540
  this.logger.info(`Fetching primary group for product: ${productId}`);
5429
6541
  const response = await this.get(endpoint);
@@ -5442,13 +6554,44 @@ class ApiClient {
5442
6554
  };
5443
6555
  }
5444
6556
  /**
5445
- * Get active groups for a product
6557
+ * Get all active groups for a product
6558
+ *
6559
+ * Retrieves a list of currently active groups for a product, including
6560
+ * member count, capacity, and whether the current user is a member.
6561
+ *
6562
+ * @param {string} productId - The product ID to fetch active groups for
6563
+ * @returns {Promise<ApiResponse<{groups: Array}>>} List of active groups with member info
5446
6564
  *
5447
- * @param productId - The product ID to fetch active groups for
5448
- * @returns Promise with active groups list
6565
+ * @description
6566
+ * Each group includes:
6567
+ * - Group ID and number
6568
+ * - Current and max participant counts
6569
+ * - Status (pending, active, fulfilled, expired)
6570
+ * - Time remaining before group closes
6571
+ * - Whether current user is a member
6572
+ * - Discount and reward information
6573
+ *
6574
+ * @example
6575
+ * ```typescript
6576
+ * const response = await client.getProductActiveGroups('prod_abc123');
6577
+ * if (response.success) {
6578
+ * response.data?.groups.forEach(group => {
6579
+ * console.log(`Group ${group.group_number}: ${group.participants_count}/${group.max_participants}`);
6580
+ * });
6581
+ * }
6582
+ * ```
5449
6583
  */
5450
6584
  async getProductActiveGroups(productId) {
5451
6585
  var _a;
6586
+ if (!validateProductId(productId)) {
6587
+ return {
6588
+ success: false,
6589
+ error: {
6590
+ message: "Invalid productId format. Must be a non-empty string (max 200 chars).",
6591
+ code: "INVALID_PRODUCT_ID",
6592
+ },
6593
+ };
6594
+ }
5452
6595
  const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_ACTIVE_GROUPS, { productId });
5453
6596
  this.logger.info(`Fetching active groups for product: ${productId}`);
5454
6597
  const response = await this.get(endpoint);
@@ -5468,6 +6611,28 @@ class ApiClient {
5468
6611
  }
5469
6612
  /**
5470
6613
  * Create a new group for a product and join it
6614
+ *
6615
+ * Creates a brand new group for a product and immediately adds the current
6616
+ * user as the first member. Useful when no existing groups meet the user's needs.
6617
+ *
6618
+ * @param {string} productId - The product ID to create a group for
6619
+ * @returns {Promise<ApiResponse<GroupJoinResponseData>>} New group info and join status
6620
+ *
6621
+ * @description
6622
+ * Returns:
6623
+ * - New group ID and number
6624
+ * - User's membership confirmation
6625
+ * - Group details (capacity, status, etc.)
6626
+ * - Discount values
6627
+ *
6628
+ * @example
6629
+ * ```typescript
6630
+ * const response = await client.createAndJoinGroup('prod_abc123');
6631
+ * if (response.success) {
6632
+ * const groupId = response.data?.group?.id;
6633
+ * console.log('Created new group:', groupId);
6634
+ * }
6635
+ * ```
5471
6636
  */
5472
6637
  async createAndJoinGroup(productId) {
5473
6638
  var _a;
@@ -5491,11 +6656,41 @@ class ApiClient {
5491
6656
  };
5492
6657
  }
5493
6658
  /**
5494
- * Join a group
6659
+ * Join an existing group
6660
+ *
6661
+ * Adds the current user to an existing group, optionally providing contact
6662
+ * information (email or phone) to enrich the session.
5495
6663
  *
5496
- * @param groupId - The group ID to join
5497
- * @param contact - Optional contact information to enrich the session
5498
- * @returns Promise with join result
6664
+ * @param {string} groupId - The group ID to join
6665
+ * @param {Contact} [contact] - Optional contact information (email or phone)
6666
+ * @param {"email" | "phone"} [contact.type] - Type of contact
6667
+ * @param {string} [contact.value] - Email address or phone number
6668
+ * @returns {Promise<ApiResponse<GroupJoinResponseData>>} Updated group info and join status
6669
+ *
6670
+ * @description
6671
+ * Actions:
6672
+ * - Adds user to group members
6673
+ * - Records contact information if provided
6674
+ * - Updates group status if member count reaches maximum
6675
+ * - Returns updated group details and member count
6676
+ *
6677
+ * @example
6678
+ * ```typescript
6679
+ * // Join without contact
6680
+ * const response = await client.joinGroup('grp_xyz789');
6681
+ *
6682
+ * // Join with email
6683
+ * const response = await client.joinGroup('grp_xyz789', {
6684
+ * type: 'email',
6685
+ * value: 'user@example.com'
6686
+ * });
6687
+ *
6688
+ * // Join with phone
6689
+ * const response = await client.joinGroup('grp_xyz789', {
6690
+ * type: 'phone',
6691
+ * value: '+1234567890'
6692
+ * });
6693
+ * ```
5499
6694
  */
5500
6695
  async joinGroup(groupId, contact) {
5501
6696
  var _a;
@@ -5529,7 +6724,38 @@ class ApiClient {
5529
6724
  };
5530
6725
  }
5531
6726
  /**
5532
- * Send an invite/share event for a group
6727
+ * Record a group share/invite event
6728
+ *
6729
+ * Tracks when a user shares or invites others to join a group via
6730
+ * a specific channel (email, SMS, social, etc.). Helps analyze referral effectiveness.
6731
+ *
6732
+ * @param {string} groupId - The group ID being shared
6733
+ * @param {ShareChannel} sharedVia - Channel used for sharing ("email", "sms", "whatsapp", "facebook", "twitter", "linkedin", "copy_link", "qr", "other")
6734
+ * @returns {Promise<ApiResponse<GroupInviteResponseData>>} Invite tracking result
6735
+ *
6736
+ * @description
6737
+ * Supported channels:
6738
+ * - email: Email invitation
6739
+ * - sms: SMS/text message
6740
+ * - whatsapp: WhatsApp share
6741
+ * - facebook: Facebook share
6742
+ * - twitter: Twitter/X share
6743
+ * - linkedin: LinkedIn share
6744
+ * - copy_link: Direct link copy
6745
+ * - qr: QR code scan
6746
+ * - other: Other sharing method
6747
+ *
6748
+ * @example
6749
+ * ```typescript
6750
+ * // Track email share
6751
+ * await client.inviteToGroup('grp_xyz789', 'email');
6752
+ *
6753
+ * // Track WhatsApp share
6754
+ * await client.inviteToGroup('grp_xyz789', 'whatsapp');
6755
+ *
6756
+ * // Track copied link
6757
+ * await client.inviteToGroup('grp_xyz789', 'copy_link');
6758
+ * ```
5533
6759
  */
5534
6760
  async inviteToGroup(groupId, sharedVia) {
5535
6761
  const endpoint = buildApiUrl("", API_ENDPOINTS.GROUP_INVITE, { groupId });
@@ -5555,13 +6781,35 @@ class ApiClient {
5555
6781
  /**
5556
6782
  * Set or update contact information for the current session
5557
6783
  *
5558
- * Sends contact information (email or phone) to link with the current session.
5559
- * This allows merchants to identify a previously anonymous user.
5560
- * Backend will not overwrite existing stored contact.
5561
- * Safe to call multiple times (idempotent).
6784
+ * Associates email or phone with the current session, allowing merchants
6785
+ * to identify a previously anonymous user without requiring form submission.
6786
+ *
6787
+ * @param {Contact} contact - Contact information
6788
+ * @param {"email" | "phone"} contact.type - Type of contact
6789
+ * @param {string} contact.value - Email address or phone number
6790
+ * @returns {Promise<ApiResponse<void>>} Success confirmation
6791
+ *
6792
+ * @description
6793
+ * Features:
6794
+ * - Idempotent: safe to call multiple times
6795
+ * - Backend won't overwrite existing contact (if any)
6796
+ * - Enables identifying returning customers
6797
+ * - Enriches session data for analytics
6798
+ *
6799
+ * @example
6800
+ * ```typescript
6801
+ * // Set email
6802
+ * await client.setContact({
6803
+ * type: 'email',
6804
+ * value: 'customer@example.com'
6805
+ * });
5562
6806
  *
5563
- * @param contact - Contact information with type and value
5564
- * @returns Promise with success status
6807
+ * // Set phone
6808
+ * await client.setContact({
6809
+ * type: 'phone',
6810
+ * value: '+1-555-123-4567'
6811
+ * });
6812
+ * ```
5565
6813
  */
5566
6814
  async setContact(contact) {
5567
6815
  const endpoint = "/v1/sdk/session/contact";
@@ -5590,11 +6838,28 @@ class ApiClient {
5590
6838
  * Prepare checkout for a group
5591
6839
  *
5592
6840
  * Signals the backend that the user is proceeding to checkout for a specific group.
5593
- * This allows the backend to prepare order data, lock pricing, and track conversion.
5594
- * Returns a checkout reference to be used in subsequent validate and confirm calls.
6841
+ * Reserves order capacity, locks pricing, prepares order data, and enables conversion tracking.
6842
+ * Must be completed before validateCheckout and confirmCheckout.
6843
+ *
6844
+ * @param {string} groupId - The group ID to prepare checkout for
6845
+ * @returns {Promise<ApiResponse<{checkout_ref: string}>>} Checkout reference for use in subsequent calls
5595
6846
  *
5596
- * @param groupId - The group ID to prepare checkout for
5597
- * @returns Promise with success status and checkout reference
6847
+ * @description
6848
+ * Actions performed:
6849
+ * - Validates group status and user membership
6850
+ * - Reserves a seat in the group
6851
+ * - Locks product reward/pricing
6852
+ * - Generates a unique checkout reference
6853
+ * - Enables server-side conversion tracking
6854
+ *
6855
+ * @example
6856
+ * ```typescript
6857
+ * const response = await client.prepareCheckout('grp_xyz789');
6858
+ * if (response.success) {
6859
+ * const checkoutRef = response.data?.checkout_ref;
6860
+ * // Store this reference for validateCheckout and confirmCheckout
6861
+ * }
6862
+ * ```
5598
6863
  */
5599
6864
  async prepareCheckout(groupId) {
5600
6865
  var _a, _b;
@@ -5622,11 +6887,31 @@ class ApiClient {
5622
6887
  * Validate checkout for a group
5623
6888
  *
5624
6889
  * Validates the checkout reference and confirms the order can proceed.
5625
- * This should be called after prepareCheckout to verify the checkout state.
6890
+ * Verifies group status, user membership, and pricing consistency.
6891
+ * Should be called after prepareCheckout and before confirmCheckout.
5626
6892
  *
5627
- * @param groupId - The group ID to validate checkout for
5628
- * @param checkoutRef - The checkout reference ID from the prepare step
5629
- * @returns Promise with success status
6893
+ * @param {string} groupId - The group ID being checked out
6894
+ * @param {string} checkoutRef - The checkout reference from prepareCheckout
6895
+ * @returns {Promise<ApiResponse<CheckoutValidationData>>} Validation result with order details
6896
+ *
6897
+ * @description
6898
+ * Validations performed:
6899
+ * - Checkout reference is valid and not expired
6900
+ * - User is still a valid group member
6901
+ * - Group hasn't changed status since prepare
6902
+ * - Pricing/reward data is consistent
6903
+ * - Order can safely proceed
6904
+ *
6905
+ * @example
6906
+ * ```typescript
6907
+ * const validateResp = await client.validateCheckout('grp_xyz789', 'chk_abc123');
6908
+ * if (validateResp.success) {
6909
+ * const order = validateResp.data;
6910
+ * console.log('Order total:', order.total);
6911
+ * console.log('Discount:', order.discount);
6912
+ * // Proceed to payment if validation passed
6913
+ * }
6914
+ * ```
5630
6915
  */
5631
6916
  async validateCheckout(groupId, checkoutRef) {
5632
6917
  var _a;
@@ -5653,12 +6938,52 @@ class ApiClient {
5653
6938
  /**
5654
6939
  * Confirm checkout for a group
5655
6940
  *
5656
- * Finalizes the checkout and confirms the order.
5657
- * This should be called after validateCheckout to complete the checkout process.
6941
+ * Finalizes and confirms the order after validation.
6942
+ * Marks the order as complete, applies discounts, records the transaction,
6943
+ * and triggers post-purchase workflows.
6944
+ * Should be called after validateCheckout to complete the checkout flow.
6945
+ *
6946
+ * @param {string} groupId - The group ID being checked out
6947
+ * @param {string} checkoutRef - The checkout reference from prepareCheckout
6948
+ * @param {CheckoutConfirmData} [data] - Optional order details
6949
+ * @param {string} [data.order_id] - Merchant's order ID for this purchase
6950
+ * @param {number} [data.order_total] - Order total amount
6951
+ * @param {number} [data.discount_applied] - Discount amount applied
6952
+ * @param {string} [data.currency] - Currency code (e.g., "USD")
6953
+ * @param {string} [data.payment_method] - Payment method used (e.g., "card", "paypal")
6954
+ * @param {boolean} [data.micrositeCheckout] - Whether this was via microsite checkout
6955
+ * @param {Record<string, unknown>} [data.metadata] - Custom metadata to attach to order
6956
+ * @returns {Promise<ApiResponse<void>>} Confirmation result
6957
+ *
6958
+ * @description
6959
+ * Actions performed after confirmation:
6960
+ * - Order is recorded in the system
6961
+ * - Discount is applied to the order
6962
+ * - Group fulfillment may be triggered if complete
6963
+ * - Merchant order system is updated
6964
+ * - Customer receives confirmation
6965
+ * - Post-purchase emails/notifications sent
6966
+ * - Analytics events recorded
5658
6967
  *
5659
- * @param groupId - The group ID to confirm checkout for
5660
- * @param checkoutRef - The checkout reference ID from the prepare step
5661
- * @returns Promise with success status
6968
+ * @example
6969
+ * ```typescript
6970
+ * // Confirm with basic reference
6971
+ * await client.confirmCheckout('grp_xyz789', 'chk_abc123');
6972
+ *
6973
+ * // Confirm with order details
6974
+ * await client.confirmCheckout('grp_xyz789', 'chk_abc123', {
6975
+ * order_id: 'ORDER-9001',
6976
+ * order_total: 99.99,
6977
+ * discount_applied: 20.00,
6978
+ * currency: 'USD',
6979
+ * payment_method: 'card',
6980
+ * metadata: {
6981
+ * coupon_code: 'WELCOME20',
6982
+ * channel: 'widget',
6983
+ * loyalty_member: true
6984
+ * }
6985
+ * });
6986
+ * ```
5662
6987
  */
5663
6988
  async confirmCheckout(groupId, checkoutRef, data) {
5664
6989
  const endpoint = buildApiUrl("", API_ENDPOINTS.GROUP_CHECKOUT_CONFIRM, { groupId });
@@ -5737,9 +7062,13 @@ class AnalyticsClient {
5737
7062
  }
5738
7063
  /**
5739
7064
  * Send event to backend analytics endpoint using unified ApiClient
7065
+ *
7066
+ * @throws {CoBuyApiError} If API request fails
7067
+ * @private
7068
+ * @internal
5740
7069
  */
5741
7070
  async sendEvent(event) {
5742
- var _a, _b;
7071
+ var _a, _b, _c, _d;
5743
7072
  if (!this.apiClient) {
5744
7073
  this.logger.warn("[Analytics] ApiClient not available, skipping event");
5745
7074
  return;
@@ -5750,14 +7079,21 @@ class AnalyticsClient {
5750
7079
  body: event,
5751
7080
  });
5752
7081
  if (!response.success) {
5753
- throw new Error(((_a = response.error) === null || _a === void 0 ? void 0 : _a.message) || "Failed to send analytics event");
7082
+ const error = new CoBuyApiError(((_a = response.error) === null || _a === void 0 ? void 0 : _a.message) || "Failed to send analytics event", ((_b = response.error) === null || _b === void 0 ? void 0 : _b.code) || "ANALYTICS_ERROR", {
7083
+ statusCode: (_c = response.error) === null || _c === void 0 ? void 0 : _c.statusCode,
7084
+ originalError: response.error,
7085
+ });
7086
+ throw error;
5754
7087
  }
5755
- this.logger.debug("[Analytics] Event recorded", { id: (_b = response.data) === null || _b === void 0 ? void 0 : _b.id });
7088
+ this.logger.debug("[Analytics] Event recorded", { id: (_d = response.data) === null || _d === void 0 ? void 0 : _d.id });
5756
7089
  }
5757
7090
  catch (error) {
5758
7091
  this.logger.error("[Analytics] Failed to send event", error);
5759
- // Re-throw for caller to handle if needed
5760
- throw error;
7092
+ // Wrap non-CoBuyApiError exceptions
7093
+ if (error instanceof CoBuyApiError) {
7094
+ throw error;
7095
+ }
7096
+ throw new CoBuyApiError(error instanceof Error ? error.message : "Unknown analytics error", "ANALYTICS_ERROR", { originalError: error });
5761
7097
  }
5762
7098
  }
5763
7099
  /**
@@ -8960,7 +10296,6 @@ class Socket extends Emitter {
8960
10296
  flags: Object.assign({ fromQueue: true }, this.flags),
8961
10297
  };
8962
10298
  args.push((err, ...responseArgs) => {
8963
- if (packet !== this._queue[0]) ;
8964
10299
  const hasError = err !== null;
8965
10300
  if (hasError) {
8966
10301
  if (packet.tryCount > this._opts.retries) {
@@ -10103,16 +11438,825 @@ class SocketManager {
10103
11438
  }
10104
11439
 
10105
11440
  /**
10106
- * SDK version constant
11441
+ * Fingerprint BotD v2.0.0 - Copyright (c) FingerprintJS, Inc, 2025 (https://fingerprint.com)
11442
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
10107
11443
  */
10108
- const SDK_VERSION = "1.0.0";
11444
+
11445
+ var version = "2.0.0";
11446
+
10109
11447
  /**
10110
- * Main CoBuy SDK implementation
11448
+ * Enum for types of bots.
11449
+ * Specific types of bots come first, followed by automation technologies.
11450
+ *
11451
+ * @readonly
11452
+ * @enum {string}
10111
11453
  */
10112
- class CoBuy {
10113
- constructor() {
10114
- this.apiClient = null;
10115
- this.analyticsClient = null;
11454
+ const BotKind = {
11455
+ // Object is used instead of Typescript enum to avoid emitting IIFE which might be affected by further tree-shaking.
11456
+ // See example of compiled enums https://stackoverflow.com/q/47363996)
11457
+ Awesomium: 'awesomium',
11458
+ Cef: 'cef',
11459
+ CefSharp: 'cefsharp',
11460
+ CoachJS: 'coachjs',
11461
+ Electron: 'electron',
11462
+ FMiner: 'fminer',
11463
+ Geb: 'geb',
11464
+ NightmareJS: 'nightmarejs',
11465
+ Phantomas: 'phantomas',
11466
+ PhantomJS: 'phantomjs',
11467
+ Rhino: 'rhino',
11468
+ Selenium: 'selenium',
11469
+ Sequentum: 'sequentum',
11470
+ SlimerJS: 'slimerjs',
11471
+ WebDriverIO: 'webdriverio',
11472
+ WebDriver: 'webdriver',
11473
+ HeadlessChrome: 'headless_chrome',
11474
+ Unknown: 'unknown',
11475
+ };
11476
+ /**
11477
+ * Bot detection error.
11478
+ */
11479
+ class BotdError extends Error {
11480
+ /**
11481
+ * Creates a new BotdError.
11482
+ *
11483
+ * @class
11484
+ */
11485
+ constructor(state, message) {
11486
+ super(message);
11487
+ this.state = state;
11488
+ this.name = 'BotdError';
11489
+ Object.setPrototypeOf(this, BotdError.prototype);
11490
+ }
11491
+ }
11492
+
11493
+ function detect(components, detectors) {
11494
+ const detections = {};
11495
+ let finalDetection = {
11496
+ bot: false,
11497
+ };
11498
+ for (const detectorName in detectors) {
11499
+ const detector = detectors[detectorName];
11500
+ const detectorRes = detector(components);
11501
+ let detection = { bot: false };
11502
+ if (typeof detectorRes === 'string') {
11503
+ detection = { bot: true, botKind: detectorRes };
11504
+ }
11505
+ else if (detectorRes) {
11506
+ detection = { bot: true, botKind: BotKind.Unknown };
11507
+ }
11508
+ detections[detectorName] = detection;
11509
+ if (detection.bot) {
11510
+ finalDetection = detection;
11511
+ }
11512
+ }
11513
+ return [detections, finalDetection];
11514
+ }
11515
+ async function collect(sources) {
11516
+ const components = {};
11517
+ const sourcesKeys = Object.keys(sources);
11518
+ await Promise.all(sourcesKeys.map(async (sourceKey) => {
11519
+ const res = sources[sourceKey];
11520
+ try {
11521
+ components[sourceKey] = {
11522
+ value: await res(),
11523
+ state: 0 /* State.Success */,
11524
+ };
11525
+ }
11526
+ catch (error) {
11527
+ if (error instanceof BotdError) {
11528
+ components[sourceKey] = {
11529
+ state: error.state,
11530
+ error: `${error.name}: ${error.message}`,
11531
+ };
11532
+ }
11533
+ else {
11534
+ components[sourceKey] = {
11535
+ state: -3 /* State.UnexpectedBehaviour */,
11536
+ error: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
11537
+ };
11538
+ }
11539
+ }
11540
+ }));
11541
+ return components;
11542
+ }
11543
+
11544
+ function detectAppVersion({ appVersion }) {
11545
+ if (appVersion.state !== 0 /* State.Success */)
11546
+ return false;
11547
+ if (/headless/i.test(appVersion.value))
11548
+ return BotKind.HeadlessChrome;
11549
+ if (/electron/i.test(appVersion.value))
11550
+ return BotKind.Electron;
11551
+ if (/slimerjs/i.test(appVersion.value))
11552
+ return BotKind.SlimerJS;
11553
+ }
11554
+
11555
+ function arrayIncludes(arr, value) {
11556
+ return arr.indexOf(value) !== -1;
11557
+ }
11558
+ function strIncludes(str, value) {
11559
+ return str.indexOf(value) !== -1;
11560
+ }
11561
+ function arrayFind(array, callback) {
11562
+ if ('find' in array)
11563
+ return array.find(callback);
11564
+ for (let i = 0; i < array.length; i++) {
11565
+ if (callback(array[i], i, array))
11566
+ return array[i];
11567
+ }
11568
+ return undefined;
11569
+ }
11570
+
11571
+ function getObjectProps(obj) {
11572
+ return Object.getOwnPropertyNames(obj);
11573
+ }
11574
+ function includes(arr, ...keys) {
11575
+ for (const key of keys) {
11576
+ if (typeof key === 'string') {
11577
+ if (arrayIncludes(arr, key))
11578
+ return true;
11579
+ }
11580
+ else {
11581
+ const match = arrayFind(arr, (value) => key.test(value));
11582
+ if (match != null)
11583
+ return true;
11584
+ }
11585
+ }
11586
+ return false;
11587
+ }
11588
+ function countTruthy(values) {
11589
+ return values.reduce((sum, value) => sum + (value ? 1 : 0), 0);
11590
+ }
11591
+
11592
+ function detectDocumentAttributes({ documentElementKeys }) {
11593
+ if (documentElementKeys.state !== 0 /* State.Success */)
11594
+ return false;
11595
+ if (includes(documentElementKeys.value, 'selenium', 'webdriver', 'driver')) {
11596
+ return BotKind.Selenium;
11597
+ }
11598
+ }
11599
+
11600
+ function detectErrorTrace({ errorTrace }) {
11601
+ if (errorTrace.state !== 0 /* State.Success */)
11602
+ return false;
11603
+ if (/PhantomJS/i.test(errorTrace.value))
11604
+ return BotKind.PhantomJS;
11605
+ }
11606
+
11607
+ function detectEvalLengthInconsistency({ evalLength, browserKind, browserEngineKind, }) {
11608
+ if (evalLength.state !== 0 /* State.Success */ ||
11609
+ browserKind.state !== 0 /* State.Success */ ||
11610
+ browserEngineKind.state !== 0 /* State.Success */)
11611
+ return;
11612
+ const length = evalLength.value;
11613
+ if (browserEngineKind.value === "unknown" /* BrowserEngineKind.Unknown */)
11614
+ return false;
11615
+ return ((length === 37 && !arrayIncludes(["webkit" /* BrowserEngineKind.Webkit */, "gecko" /* BrowserEngineKind.Gecko */], browserEngineKind.value)) ||
11616
+ (length === 39 && !arrayIncludes(["internet_explorer" /* BrowserKind.IE */], browserKind.value)) ||
11617
+ (length === 33 && !arrayIncludes(["chromium" /* BrowserEngineKind.Chromium */], browserEngineKind.value)));
11618
+ }
11619
+
11620
+ function detectFunctionBind({ functionBind }) {
11621
+ if (functionBind.state === -2 /* State.NotFunction */)
11622
+ return BotKind.PhantomJS;
11623
+ }
11624
+
11625
+ function detectLanguagesLengthInconsistency({ languages }) {
11626
+ if (languages.state === 0 /* State.Success */ && languages.value.length === 0) {
11627
+ return BotKind.HeadlessChrome;
11628
+ }
11629
+ }
11630
+
11631
+ function detectMimeTypesConsistent({ mimeTypesConsistent }) {
11632
+ if (mimeTypesConsistent.state === 0 /* State.Success */ && !mimeTypesConsistent.value) {
11633
+ return BotKind.Unknown;
11634
+ }
11635
+ }
11636
+
11637
+ function detectNotificationPermissions({ notificationPermissions, browserKind, }) {
11638
+ if (browserKind.state !== 0 /* State.Success */ || browserKind.value !== "chrome" /* BrowserKind.Chrome */)
11639
+ return false;
11640
+ if (notificationPermissions.state === 0 /* State.Success */ && notificationPermissions.value) {
11641
+ return BotKind.HeadlessChrome;
11642
+ }
11643
+ }
11644
+
11645
+ function detectPluginsArray({ pluginsArray }) {
11646
+ if (pluginsArray.state === 0 /* State.Success */ && !pluginsArray.value)
11647
+ return BotKind.HeadlessChrome;
11648
+ }
11649
+
11650
+ function detectPluginsLengthInconsistency({ pluginsLength, android, browserKind, browserEngineKind, }) {
11651
+ if (pluginsLength.state !== 0 /* State.Success */ ||
11652
+ android.state !== 0 /* State.Success */ ||
11653
+ browserKind.state !== 0 /* State.Success */ ||
11654
+ browserEngineKind.state !== 0 /* State.Success */)
11655
+ return;
11656
+ if (browserKind.value !== "chrome" /* BrowserKind.Chrome */ ||
11657
+ android.value ||
11658
+ browserEngineKind.value !== "chromium" /* BrowserEngineKind.Chromium */)
11659
+ return;
11660
+ if (pluginsLength.value === 0)
11661
+ return BotKind.HeadlessChrome;
11662
+ }
11663
+
11664
+ function detectProcess({ process }) {
11665
+ var _a;
11666
+ if (process.state !== 0 /* State.Success */)
11667
+ return false;
11668
+ if (process.value.type === 'renderer' || ((_a = process.value.versions) === null || _a === void 0 ? void 0 : _a.electron) != null)
11669
+ return BotKind.Electron;
11670
+ }
11671
+
11672
+ function detectProductSub({ productSub, browserKind }) {
11673
+ if (productSub.state !== 0 /* State.Success */ || browserKind.state !== 0 /* State.Success */)
11674
+ return false;
11675
+ if ((browserKind.value === "chrome" /* BrowserKind.Chrome */ ||
11676
+ browserKind.value === "safari" /* BrowserKind.Safari */ ||
11677
+ browserKind.value === "opera" /* BrowserKind.Opera */ ||
11678
+ browserKind.value === "wechat" /* BrowserKind.WeChat */) &&
11679
+ productSub.value !== '20030107')
11680
+ return BotKind.Unknown;
11681
+ }
11682
+
11683
+ function detectUserAgent({ userAgent }) {
11684
+ if (userAgent.state !== 0 /* State.Success */)
11685
+ return false;
11686
+ if (/PhantomJS/i.test(userAgent.value))
11687
+ return BotKind.PhantomJS;
11688
+ if (/Headless/i.test(userAgent.value))
11689
+ return BotKind.HeadlessChrome;
11690
+ if (/Electron/i.test(userAgent.value))
11691
+ return BotKind.Electron;
11692
+ if (/slimerjs/i.test(userAgent.value))
11693
+ return BotKind.SlimerJS;
11694
+ }
11695
+
11696
+ function detectWebDriver({ webDriver }) {
11697
+ if (webDriver.state === 0 /* State.Success */ && webDriver.value)
11698
+ return BotKind.HeadlessChrome;
11699
+ }
11700
+
11701
+ function detectWebGL({ webGL }) {
11702
+ if (webGL.state === 0 /* State.Success */) {
11703
+ const { vendor, renderer } = webGL.value;
11704
+ if (vendor == 'Brian Paul' && renderer == 'Mesa OffScreen') {
11705
+ return BotKind.HeadlessChrome;
11706
+ }
11707
+ }
11708
+ }
11709
+
11710
+ function detectWindowExternal({ windowExternal }) {
11711
+ if (windowExternal.state !== 0 /* State.Success */)
11712
+ return false;
11713
+ if (/Sequentum/i.test(windowExternal.value))
11714
+ return BotKind.Sequentum;
11715
+ }
11716
+
11717
+ function detectWindowSize({ windowSize, documentFocus }) {
11718
+ if (windowSize.state !== 0 /* State.Success */ || documentFocus.state !== 0 /* State.Success */)
11719
+ return false;
11720
+ const { outerWidth, outerHeight } = windowSize.value;
11721
+ // When a page is opened in a new tab without focusing it right away, the window outer size is 0x0
11722
+ if (!documentFocus.value)
11723
+ return;
11724
+ if (outerWidth === 0 && outerHeight === 0)
11725
+ return BotKind.HeadlessChrome;
11726
+ }
11727
+
11728
+ function detectDistinctiveProperties({ distinctiveProps }) {
11729
+ if (distinctiveProps.state !== 0 /* State.Success */)
11730
+ return false;
11731
+ const value = distinctiveProps.value;
11732
+ let bot;
11733
+ for (bot in value)
11734
+ if (value[bot])
11735
+ return bot;
11736
+ }
11737
+
11738
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
11739
+ const detectors = {
11740
+ detectAppVersion,
11741
+ detectDocumentAttributes,
11742
+ detectErrorTrace,
11743
+ detectEvalLengthInconsistency,
11744
+ detectFunctionBind,
11745
+ detectLanguagesLengthInconsistency,
11746
+ detectNotificationPermissions,
11747
+ detectPluginsArray,
11748
+ detectPluginsLengthInconsistency,
11749
+ detectProcess,
11750
+ detectUserAgent,
11751
+ detectWebDriver,
11752
+ detectWebGL,
11753
+ detectWindowExternal,
11754
+ detectWindowSize,
11755
+ detectMimeTypesConsistent,
11756
+ detectProductSub,
11757
+ detectDistinctiveProperties,
11758
+ };
11759
+
11760
+ function getAppVersion() {
11761
+ const appVersion = navigator.appVersion;
11762
+ if (appVersion == undefined) {
11763
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.appVersion is undefined');
11764
+ }
11765
+ return appVersion;
11766
+ }
11767
+
11768
+ function getDocumentElementKeys() {
11769
+ if (document.documentElement === undefined) {
11770
+ throw new BotdError(-1 /* State.Undefined */, 'document.documentElement is undefined');
11771
+ }
11772
+ const { documentElement } = document;
11773
+ if (typeof documentElement.getAttributeNames !== 'function') {
11774
+ throw new BotdError(-2 /* State.NotFunction */, 'document.documentElement.getAttributeNames is not a function');
11775
+ }
11776
+ return documentElement.getAttributeNames();
11777
+ }
11778
+
11779
+ function getErrorTrace() {
11780
+ try {
11781
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
11782
+ // @ts-ignore
11783
+ null[0]();
11784
+ }
11785
+ catch (error) {
11786
+ if (error instanceof Error && error['stack'] != null) {
11787
+ return error.stack.toString();
11788
+ }
11789
+ }
11790
+ throw new BotdError(-3 /* State.UnexpectedBehaviour */, 'errorTrace signal unexpected behaviour');
11791
+ }
11792
+
11793
+ function getEvalLength() {
11794
+ return eval.toString().length;
11795
+ }
11796
+
11797
+ function getFunctionBind() {
11798
+ if (Function.prototype.bind === undefined) {
11799
+ throw new BotdError(-2 /* State.NotFunction */, 'Function.prototype.bind is undefined');
11800
+ }
11801
+ return Function.prototype.bind.toString();
11802
+ }
11803
+
11804
+ function getBrowserEngineKind() {
11805
+ var _a, _b;
11806
+ // Based on research in October 2020. Tested to detect Chromium 42-86.
11807
+ const w = window;
11808
+ const n = navigator;
11809
+ if (countTruthy([
11810
+ 'webkitPersistentStorage' in n,
11811
+ 'webkitTemporaryStorage' in n,
11812
+ n.vendor.indexOf('Google') === 0,
11813
+ 'webkitResolveLocalFileSystemURL' in w,
11814
+ 'BatteryManager' in w,
11815
+ 'webkitMediaStream' in w,
11816
+ 'webkitSpeechGrammar' in w,
11817
+ ]) >= 5) {
11818
+ return "chromium" /* BrowserEngineKind.Chromium */;
11819
+ }
11820
+ if (countTruthy([
11821
+ 'ApplePayError' in w,
11822
+ 'CSSPrimitiveValue' in w,
11823
+ 'Counter' in w,
11824
+ n.vendor.indexOf('Apple') === 0,
11825
+ 'getStorageUpdates' in n,
11826
+ 'WebKitMediaKeys' in w,
11827
+ ]) >= 4) {
11828
+ return "webkit" /* BrowserEngineKind.Webkit */;
11829
+ }
11830
+ if (countTruthy([
11831
+ 'buildID' in navigator,
11832
+ 'MozAppearance' in ((_b = (_a = document.documentElement) === null || _a === void 0 ? void 0 : _a.style) !== null && _b !== void 0 ? _b : {}),
11833
+ 'onmozfullscreenchange' in w,
11834
+ 'mozInnerScreenX' in w,
11835
+ 'CSSMozDocumentRule' in w,
11836
+ 'CanvasCaptureMediaStream' in w,
11837
+ ]) >= 4) {
11838
+ return "gecko" /* BrowserEngineKind.Gecko */;
11839
+ }
11840
+ return "unknown" /* BrowserEngineKind.Unknown */;
11841
+ }
11842
+ function getBrowserKind() {
11843
+ var _a;
11844
+ const userAgent = (_a = navigator.userAgent) === null || _a === void 0 ? void 0 : _a.toLowerCase();
11845
+ if (strIncludes(userAgent, 'edg/')) {
11846
+ return "edge" /* BrowserKind.Edge */;
11847
+ }
11848
+ else if (strIncludes(userAgent, 'trident') || strIncludes(userAgent, 'msie')) {
11849
+ return "internet_explorer" /* BrowserKind.IE */;
11850
+ }
11851
+ else if (strIncludes(userAgent, 'wechat')) {
11852
+ return "wechat" /* BrowserKind.WeChat */;
11853
+ }
11854
+ else if (strIncludes(userAgent, 'firefox')) {
11855
+ return "firefox" /* BrowserKind.Firefox */;
11856
+ }
11857
+ else if (strIncludes(userAgent, 'opera') || strIncludes(userAgent, 'opr')) {
11858
+ return "opera" /* BrowserKind.Opera */;
11859
+ }
11860
+ else if (strIncludes(userAgent, 'chrome')) {
11861
+ return "chrome" /* BrowserKind.Chrome */;
11862
+ }
11863
+ else if (strIncludes(userAgent, 'safari')) {
11864
+ return "safari" /* BrowserKind.Safari */;
11865
+ }
11866
+ else {
11867
+ return "unknown" /* BrowserKind.Unknown */;
11868
+ }
11869
+ }
11870
+ // Source: https://github.com/fingerprintjs/fingerprintjs/blob/master/src/utils/browser.ts#L223
11871
+ function isAndroid() {
11872
+ const browserEngineKind = getBrowserEngineKind();
11873
+ const isItChromium = browserEngineKind === "chromium" /* BrowserEngineKind.Chromium */;
11874
+ const isItGecko = browserEngineKind === "gecko" /* BrowserEngineKind.Gecko */;
11875
+ const w = window;
11876
+ const n = navigator;
11877
+ const c = 'connection';
11878
+ // Chrome removes all words "Android" from `navigator` when desktop version is requested
11879
+ // Firefox keeps "Android" in `navigator.appVersion` when desktop version is requested
11880
+ if (isItChromium) {
11881
+ return (countTruthy([
11882
+ !('SharedWorker' in w),
11883
+ // `typechange` is deprecated, but it's still present on Android (tested on Chrome Mobile 117)
11884
+ // Removal proposal https://bugs.chromium.org/p/chromium/issues/detail?id=699892
11885
+ // Note: this expression returns true on ChromeOS, so additional detectors are required to avoid false-positives
11886
+ n[c] && 'ontypechange' in n[c],
11887
+ !('sinkId' in new Audio()),
11888
+ ]) >= 2);
11889
+ }
11890
+ else if (isItGecko) {
11891
+ return countTruthy(['onorientationchange' in w, 'orientation' in w, /android/i.test(n.appVersion)]) >= 2;
11892
+ }
11893
+ else {
11894
+ // Only 2 browser engines are presented on Android.
11895
+ // Actually, there is also Android 4.1 browser, but it's not worth detecting it at the moment.
11896
+ return false;
11897
+ }
11898
+ }
11899
+ function getDocumentFocus() {
11900
+ if (document.hasFocus === undefined) {
11901
+ return false;
11902
+ }
11903
+ return document.hasFocus();
11904
+ }
11905
+ function isChromium86OrNewer() {
11906
+ // Checked in Chrome 85 vs Chrome 86 both on desktop and Android. Checked in macOS Chrome 128, Android Chrome 127.
11907
+ const w = window;
11908
+ return (countTruthy([
11909
+ !('MediaSettingsRange' in w),
11910
+ 'RTCEncodedAudioFrame' in w,
11911
+ '' + w.Intl === '[object Intl]',
11912
+ '' + w.Reflect === '[object Reflect]',
11913
+ ]) >= 3);
11914
+ }
11915
+
11916
+ function getLanguages() {
11917
+ const n = navigator;
11918
+ const result = [];
11919
+ const language = n.language || n.userLanguage || n.browserLanguage || n.systemLanguage;
11920
+ if (language !== undefined) {
11921
+ result.push([language]);
11922
+ }
11923
+ if (Array.isArray(n.languages)) {
11924
+ const browserEngine = getBrowserEngineKind();
11925
+ // Starting from Chromium 86, there is only a single value in `navigator.language` in Incognito mode:
11926
+ // the value of `navigator.language`. Therefore, the value is ignored in this browser.
11927
+ if (!(browserEngine === "chromium" /* BrowserEngineKind.Chromium */ && isChromium86OrNewer())) {
11928
+ result.push(n.languages);
11929
+ }
11930
+ }
11931
+ else if (typeof n.languages === 'string') {
11932
+ const languages = n.languages;
11933
+ if (languages) {
11934
+ result.push(languages.split(','));
11935
+ }
11936
+ }
11937
+ return result;
11938
+ }
11939
+
11940
+ function areMimeTypesConsistent() {
11941
+ if (navigator.mimeTypes === undefined) {
11942
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.mimeTypes is undefined');
11943
+ }
11944
+ const { mimeTypes } = navigator;
11945
+ let isConsistent = Object.getPrototypeOf(mimeTypes) === MimeTypeArray.prototype;
11946
+ for (let i = 0; i < mimeTypes.length; i++) {
11947
+ isConsistent && (isConsistent = Object.getPrototypeOf(mimeTypes[i]) === MimeType.prototype);
11948
+ }
11949
+ return isConsistent;
11950
+ }
11951
+
11952
+ async function getNotificationPermissions() {
11953
+ if (window.Notification === undefined) {
11954
+ throw new BotdError(-1 /* State.Undefined */, 'window.Notification is undefined');
11955
+ }
11956
+ if (navigator.permissions === undefined) {
11957
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.permissions is undefined');
11958
+ }
11959
+ const { permissions } = navigator;
11960
+ if (typeof permissions.query !== 'function') {
11961
+ throw new BotdError(-2 /* State.NotFunction */, 'navigator.permissions.query is not a function');
11962
+ }
11963
+ try {
11964
+ const permissionStatus = await permissions.query({ name: 'notifications' });
11965
+ return window.Notification.permission === 'denied' && permissionStatus.state === 'prompt';
11966
+ }
11967
+ catch (e) {
11968
+ throw new BotdError(-3 /* State.UnexpectedBehaviour */, 'notificationPermissions signal unexpected behaviour');
11969
+ }
11970
+ }
11971
+
11972
+ function getPluginsArray() {
11973
+ if (navigator.plugins === undefined) {
11974
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.plugins is undefined');
11975
+ }
11976
+ if (window.PluginArray === undefined) {
11977
+ throw new BotdError(-1 /* State.Undefined */, 'window.PluginArray is undefined');
11978
+ }
11979
+ return navigator.plugins instanceof PluginArray;
11980
+ }
11981
+
11982
+ function getPluginsLength() {
11983
+ if (navigator.plugins === undefined) {
11984
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.plugins is undefined');
11985
+ }
11986
+ if (navigator.plugins.length === undefined) {
11987
+ throw new BotdError(-3 /* State.UnexpectedBehaviour */, 'navigator.plugins.length is undefined');
11988
+ }
11989
+ return navigator.plugins.length;
11990
+ }
11991
+
11992
+ function getProcess() {
11993
+ const { process } = window;
11994
+ const errorPrefix = 'window.process is';
11995
+ if (process === undefined) {
11996
+ throw new BotdError(-1 /* State.Undefined */, `${errorPrefix} undefined`);
11997
+ }
11998
+ if (process && typeof process !== 'object') {
11999
+ throw new BotdError(-3 /* State.UnexpectedBehaviour */, `${errorPrefix} not an object`);
12000
+ }
12001
+ return process;
12002
+ }
12003
+
12004
+ function getProductSub() {
12005
+ const { productSub } = navigator;
12006
+ if (productSub === undefined) {
12007
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.productSub is undefined');
12008
+ }
12009
+ return productSub;
12010
+ }
12011
+
12012
+ function getRTT() {
12013
+ if (navigator.connection === undefined) {
12014
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.connection is undefined');
12015
+ }
12016
+ if (navigator.connection.rtt === undefined) {
12017
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.connection.rtt is undefined');
12018
+ }
12019
+ return navigator.connection.rtt;
12020
+ }
12021
+
12022
+ function getUserAgent() {
12023
+ return navigator.userAgent;
12024
+ }
12025
+
12026
+ function getWebDriver() {
12027
+ if (navigator.webdriver == undefined) {
12028
+ throw new BotdError(-1 /* State.Undefined */, 'navigator.webdriver is undefined');
12029
+ }
12030
+ return navigator.webdriver;
12031
+ }
12032
+
12033
+ function getWebGL() {
12034
+ const canvasElement = document.createElement('canvas');
12035
+ if (typeof canvasElement.getContext !== 'function') {
12036
+ throw new BotdError(-2 /* State.NotFunction */, 'HTMLCanvasElement.getContext is not a function');
12037
+ }
12038
+ const webGLContext = canvasElement.getContext('webgl');
12039
+ if (webGLContext === null) {
12040
+ throw new BotdError(-4 /* State.Null */, 'WebGLRenderingContext is null');
12041
+ }
12042
+ if (typeof webGLContext.getParameter !== 'function') {
12043
+ throw new BotdError(-2 /* State.NotFunction */, 'WebGLRenderingContext.getParameter is not a function');
12044
+ }
12045
+ const vendor = webGLContext.getParameter(webGLContext.VENDOR);
12046
+ const renderer = webGLContext.getParameter(webGLContext.RENDERER);
12047
+ return { vendor: vendor, renderer: renderer };
12048
+ }
12049
+
12050
+ function getWindowExternal() {
12051
+ if (window.external === undefined) {
12052
+ throw new BotdError(-1 /* State.Undefined */, 'window.external is undefined');
12053
+ }
12054
+ const { external } = window;
12055
+ if (typeof external.toString !== 'function') {
12056
+ throw new BotdError(-2 /* State.NotFunction */, 'window.external.toString is not a function');
12057
+ }
12058
+ return external.toString();
12059
+ }
12060
+
12061
+ function getWindowSize() {
12062
+ return {
12063
+ outerWidth: window.outerWidth,
12064
+ outerHeight: window.outerHeight,
12065
+ innerWidth: window.innerWidth,
12066
+ innerHeight: window.innerHeight,
12067
+ };
12068
+ }
12069
+
12070
+ function checkDistinctiveProperties() {
12071
+ // The order in the following list matters, because specific types of bots come first, followed by automation technologies.
12072
+ const distinctivePropsList = {
12073
+ [BotKind.Awesomium]: {
12074
+ window: ['awesomium'],
12075
+ },
12076
+ [BotKind.Cef]: {
12077
+ window: ['RunPerfTest'],
12078
+ },
12079
+ [BotKind.CefSharp]: {
12080
+ window: ['CefSharp'],
12081
+ },
12082
+ [BotKind.CoachJS]: {
12083
+ window: ['emit'],
12084
+ },
12085
+ [BotKind.FMiner]: {
12086
+ window: ['fmget_targets'],
12087
+ },
12088
+ [BotKind.Geb]: {
12089
+ window: ['geb'],
12090
+ },
12091
+ [BotKind.NightmareJS]: {
12092
+ window: ['__nightmare', 'nightmare'],
12093
+ },
12094
+ [BotKind.Phantomas]: {
12095
+ window: ['__phantomas'],
12096
+ },
12097
+ [BotKind.PhantomJS]: {
12098
+ window: ['callPhantom', '_phantom'],
12099
+ },
12100
+ [BotKind.Rhino]: {
12101
+ window: ['spawn'],
12102
+ },
12103
+ [BotKind.Selenium]: {
12104
+ window: ['_Selenium_IDE_Recorder', '_selenium', 'calledSelenium', /^([a-z]){3}_.*_(Array|Promise|Symbol)$/],
12105
+ document: ['__selenium_evaluate', 'selenium-evaluate', '__selenium_unwrapped'],
12106
+ },
12107
+ [BotKind.WebDriverIO]: {
12108
+ window: ['wdioElectron'],
12109
+ },
12110
+ [BotKind.WebDriver]: {
12111
+ window: [
12112
+ 'webdriver',
12113
+ '__webdriverFunc',
12114
+ '__lastWatirAlert',
12115
+ '__lastWatirConfirm',
12116
+ '__lastWatirPrompt',
12117
+ '_WEBDRIVER_ELEM_CACHE',
12118
+ 'ChromeDriverw',
12119
+ ],
12120
+ document: [
12121
+ '__webdriver_script_fn',
12122
+ '__driver_evaluate',
12123
+ '__webdriver_evaluate',
12124
+ '__fxdriver_evaluate',
12125
+ '__driver_unwrapped',
12126
+ '__webdriver_unwrapped',
12127
+ '__fxdriver_unwrapped',
12128
+ '__webdriver_script_fn',
12129
+ '__webdriver_script_func',
12130
+ '__webdriver_script_function',
12131
+ '$cdc_asdjflasutopfhvcZLmcf',
12132
+ '$cdc_asdjflasutopfhvcZLmcfl_',
12133
+ '$chrome_asyncScriptInfo',
12134
+ '__$webdriverAsyncExecutor',
12135
+ ],
12136
+ },
12137
+ [BotKind.HeadlessChrome]: {
12138
+ window: ['domAutomation', 'domAutomationController'],
12139
+ },
12140
+ };
12141
+ let botName;
12142
+ const result = {};
12143
+ const windowProps = getObjectProps(window);
12144
+ let documentProps = [];
12145
+ if (window.document !== undefined)
12146
+ documentProps = getObjectProps(window.document);
12147
+ for (botName in distinctivePropsList) {
12148
+ const props = distinctivePropsList[botName];
12149
+ if (props !== undefined) {
12150
+ const windowContains = props.window === undefined ? false : includes(windowProps, ...props.window);
12151
+ const documentContains = props.document === undefined || !documentProps.length ? false : includes(documentProps, ...props.document);
12152
+ result[botName] = windowContains || documentContains;
12153
+ }
12154
+ }
12155
+ return result;
12156
+ }
12157
+
12158
+ const sources = {
12159
+ android: isAndroid,
12160
+ browserKind: getBrowserKind,
12161
+ browserEngineKind: getBrowserEngineKind,
12162
+ documentFocus: getDocumentFocus,
12163
+ userAgent: getUserAgent,
12164
+ appVersion: getAppVersion,
12165
+ rtt: getRTT,
12166
+ windowSize: getWindowSize,
12167
+ pluginsLength: getPluginsLength,
12168
+ pluginsArray: getPluginsArray,
12169
+ errorTrace: getErrorTrace,
12170
+ productSub: getProductSub,
12171
+ windowExternal: getWindowExternal,
12172
+ mimeTypesConsistent: areMimeTypesConsistent,
12173
+ evalLength: getEvalLength,
12174
+ webGL: getWebGL,
12175
+ webDriver: getWebDriver,
12176
+ languages: getLanguages,
12177
+ notificationPermissions: getNotificationPermissions,
12178
+ documentElementKeys: getDocumentElementKeys,
12179
+ functionBind: getFunctionBind,
12180
+ process: getProcess,
12181
+ distinctiveProps: checkDistinctiveProperties,
12182
+ };
12183
+
12184
+ /**
12185
+ * Class representing a bot detector.
12186
+ *
12187
+ * @class
12188
+ * @implements {BotDetectorInterface}
12189
+ */
12190
+ class BotDetector {
12191
+ constructor() {
12192
+ this.components = undefined;
12193
+ this.detections = undefined;
12194
+ }
12195
+ getComponents() {
12196
+ return this.components;
12197
+ }
12198
+ getDetections() {
12199
+ return this.detections;
12200
+ }
12201
+ /**
12202
+ * @inheritdoc
12203
+ */
12204
+ detect() {
12205
+ if (this.components === undefined) {
12206
+ throw new Error("BotDetector.detect can't be called before BotDetector.collect");
12207
+ }
12208
+ const [detections, finalDetection] = detect(this.components, detectors);
12209
+ this.detections = detections;
12210
+ return finalDetection;
12211
+ }
12212
+ /**
12213
+ * @inheritdoc
12214
+ */
12215
+ async collect() {
12216
+ this.components = await collect(sources);
12217
+ return this.components;
12218
+ }
12219
+ }
12220
+
12221
+ /**
12222
+ * Sends an unpersonalized AJAX request to collect installation statistics
12223
+ */
12224
+ function monitor() {
12225
+ // The FingerprintJS CDN (https://github.com/fingerprintjs/cdn) replaces `window.__fpjs_d_m` with `true`
12226
+ if (window.__fpjs_d_m || Math.random() >= 0.001) {
12227
+ return;
12228
+ }
12229
+ try {
12230
+ const request = new XMLHttpRequest();
12231
+ request.open('get', `https://m1.openfpcdn.io/botd/v${version}/npm-monitoring`, true);
12232
+ request.send();
12233
+ }
12234
+ catch (error) {
12235
+ // console.error is ok here because it's an unexpected error handler
12236
+ // eslint-disable-next-line no-console
12237
+ console.error(error);
12238
+ }
12239
+ }
12240
+ async function load({ monitoring = true } = {}) {
12241
+ if (monitoring) {
12242
+ monitor();
12243
+ }
12244
+ const detector = new BotDetector();
12245
+ await detector.collect();
12246
+ return detector;
12247
+ }
12248
+
12249
+ /**
12250
+ * SDK version constant
12251
+ */
12252
+ const SDK_VERSION = "1.0.0";
12253
+ /**
12254
+ * Main CoBuy SDK implementation
12255
+ */
12256
+ class CoBuy {
12257
+ constructor() {
12258
+ this.apiClient = null;
12259
+ this.analyticsClient = null;
10116
12260
  this.sessionId = "";
10117
12261
  this.SESSION_STORAGE_KEY = "cobuy_sdk_session_id";
10118
12262
  this.CHECKOUT_REF_PREFIX = "cobuy_checkout_ref";
@@ -10219,6 +12363,49 @@ class CoBuy {
10219
12363
  getSessionId() {
10220
12364
  return this.sessionId;
10221
12365
  }
12366
+ /**
12367
+ * Initialize BotD detection and store score for API requests
12368
+ */
12369
+ async initializeBotd() {
12370
+ if (typeof window === "undefined") {
12371
+ return;
12372
+ }
12373
+ try {
12374
+ const botd = await load();
12375
+ const result = botd.detect();
12376
+ const score = typeof result.score === "number" ? result.score : result.bot ? 1 : 0;
12377
+ this.botdScore = score;
12378
+ if (this.apiClient) {
12379
+ this.apiClient.setBotdScore(score);
12380
+ }
12381
+ this.logger.debug(`[SDK] BotD initialized with score: ${score}`);
12382
+ }
12383
+ catch (error) {
12384
+ this.logger.warn("[SDK] BotD initialization failed", error);
12385
+ }
12386
+ }
12387
+ handleFraudError(error, context) {
12388
+ var _a;
12389
+ if (!error || error.code !== "FRAUD_DETECTED") {
12390
+ return false;
12391
+ }
12392
+ try {
12393
+ const config = this.configManager.getConfig();
12394
+ if ((_a = config.events) === null || _a === void 0 ? void 0 : _a.onFraudDetected) {
12395
+ config.events.onFraudDetected({
12396
+ code: error.code,
12397
+ message: error.message || "Request blocked by fraud detection",
12398
+ statusCode: error.statusCode,
12399
+ context,
12400
+ });
12401
+ }
12402
+ }
12403
+ catch (e) {
12404
+ this.logger.warn("[SDK] Fraud handler failed", e);
12405
+ }
12406
+ this.logger.warn("[SDK] Fraud detected", { context, error });
12407
+ return true;
12408
+ }
10222
12409
  /**
10223
12410
  * Prepare checkout for a group if not already prepared
10224
12411
  * Checks localStorage to prevent duplicate calls for the same product/group/session
@@ -10246,6 +12433,9 @@ class CoBuy {
10246
12433
  this.logger.info(`[SDK] Checkout prepared and stored for ${productId}:${groupId} - Ref: ${checkoutRef}`);
10247
12434
  }
10248
12435
  else {
12436
+ if (this.handleFraudError(response.error, "prepareCheckout")) {
12437
+ return;
12438
+ }
10249
12439
  this.logger.error("[SDK] Failed to prepare checkout", response.error);
10250
12440
  }
10251
12441
  }
@@ -10254,7 +12444,49 @@ class CoBuy {
10254
12444
  }
10255
12445
  }
10256
12446
  /**
10257
- * Initialize the SDK with configuration
12447
+ * Initialize the CoBuy SDK with configuration
12448
+ *
12449
+ * Must be called before rendering widgets or opening modals.
12450
+ *
12451
+ * @param {CoBuyInitOptions} options - SDK configuration options
12452
+ * @param {"public" | "token"} options.authMode - Authentication mode (public key or session token)
12453
+ * @param {string} [options.merchantKey] - Merchant public key (required for public mode)
12454
+ * @param {string | (() => string | Promise<string>)} [options.sessionToken] - Session token (required for token mode)
12455
+ * @param {"dev" | "prod"} [options.env] - Environment (dev or prod, defaults to prod)
12456
+ * @param {boolean} [options.debug] - Enable debug logging
12457
+ * @param {Object} [options.auth] - Optional auth callbacks (onAuthError, onTokenExpired)
12458
+ * @param {Object} [options.events] - Optional event handlers (onGroupFulfilled, onGroupCreated, onGroupMemberJoined, onModalOpen, onModalClose)
12459
+ * @param {Object} [options.customHeaders] - Optional custom headers to include in API requests
12460
+ *
12461
+ * @throws {Error} If required configuration is missing or invalid
12462
+ *
12463
+ * @example
12464
+ * ```typescript
12465
+ * // Initialize with public key authentication
12466
+ * CoBuy.init({
12467
+ * authMode: 'public',
12468
+ * merchantKey: 'pk_live_abc123xyz',
12469
+ * env: 'prod',
12470
+ * debug: false
12471
+ * });
12472
+ *
12473
+ * // Initialize with session token
12474
+ * CoBuy.init({
12475
+ * authMode: 'token',
12476
+ * sessionToken: 'token_abc123xyz',
12477
+ * env: 'prod'
12478
+ * });
12479
+ *
12480
+ * // Initialize with async token provider
12481
+ * CoBuy.init({
12482
+ * authMode: 'token',
12483
+ * sessionToken: async () => {
12484
+ * const response = await fetch('/api/token');
12485
+ * return response.json().token;
12486
+ * },
12487
+ * env: 'prod'
12488
+ * });
12489
+ * ```
10258
12490
  */
10259
12491
  init(options) {
10260
12492
  var _a, _b, _c;
@@ -10288,7 +12520,14 @@ class CoBuy {
10288
12520
  baseUrl: config.apiBaseUrl,
10289
12521
  authStrategy,
10290
12522
  sessionId: this.sessionId,
12523
+ botdScore: this.botdScore,
10291
12524
  debug: config.debug,
12525
+ timeout: config.performance.requestTimeout,
12526
+ maxRetries: config.performance.maxRetries,
12527
+ });
12528
+ // Initialize BotD detection in background (non-blocking)
12529
+ this.initializeBotd().catch((error) => {
12530
+ this.logger.warn("[SDK] BotD initialization error", error);
10292
12531
  });
10293
12532
  // Initialize Analytics client with the SDK's session ID
10294
12533
  this.analyticsClient = new AnalyticsClient(config.merchantKey || "token-mode", // Merchant key not used in token mode analytics
@@ -10354,37 +12593,131 @@ class CoBuy {
10354
12593
  }
10355
12594
  /**
10356
12595
  * Render the CoBuy widget into a DOM container
12596
+ *
12597
+ * Creates and renders a collaborative buying widget for a specific product.
12598
+ * The widget displays group status, members, and allows users to join groups.
12599
+ *
12600
+ * @param {RenderWidgetOptions} options - Widget rendering options
12601
+ * @param {string} options.productId - Product ID to render widget for (required)
12602
+ * @param {string | HTMLElement} options.container - DOM selector or element to render into (required)
12603
+ * @param {Object} [options.theme] - Optional theme configuration (colors, fonts, animations)
12604
+ * @param {Function} [options.onGroupSelect] - Callback when user selects a group
12605
+ * @param {Function} [options.onError] - Callback for rendering errors (optional, errors also throw)
12606
+ *
12607
+ * @returns {Promise<void>} Resolves when widget is rendered, rejects if rendering fails
12608
+ *
12609
+ * @throws {CoBuyNotInitializedError} If SDK not initialized before calling
12610
+ * @throws {CoBuyRenderError} If container not found or rendering fails
12611
+ * @throws {CoBuyValidationError} If productId validation fails
12612
+ *
12613
+ * @description
12614
+ * Error Handling:
12615
+ * - Synchronous errors (not initialized, missing productId) are thrown immediately
12616
+ * - Asynchronous errors (network, container not found) cause Promise rejection
12617
+ * - onError callback is called with error details (for backwards compatibility)
12618
+ * - Always handle Promise rejection OR use try/catch for synchronous errors
12619
+ *
12620
+ * @example
12621
+ * ```typescript
12622
+ * // Option 1: Using try/catch (recommended)
12623
+ * try {
12624
+ * await CoBuy.renderWidget({
12625
+ * productId: 'prod_abc123',
12626
+ * container: '#cobuy-widget-container'
12627
+ * });
12628
+ * } catch (error) {
12629
+ * if (error instanceof CoBuyNotInitializedError) {
12630
+ * console.error('SDK not initialized');
12631
+ * } else if (error instanceof CoBuyRenderError) {
12632
+ * console.error('Widget render failed:', error.message);
12633
+ * }
12634
+ * }
12635
+ *
12636
+ * // Option 2: Using .catch() (backwards compatible)
12637
+ * CoBuy.renderWidget({
12638
+ * productId: 'prod_abc123',
12639
+ * container: '#widget'
12640
+ * }).catch(error => {
12641
+ * console.error('Widget error:', error);
12642
+ * });
12643
+ *
12644
+ * // Option 3: Using onError callback + error throwing
12645
+ * CoBuy.renderWidget({
12646
+ * productId: 'prod_abc123',
12647
+ * container: '#widget',
12648
+ * onError: (error) => console.error('Async error:', error)
12649
+ * }).catch(error => console.error('Promise rejected:', error));
12650
+ * ```
10357
12651
  */
10358
- renderWidget(options) {
10359
- // Check if SDK is initialized
12652
+ async renderWidget(options) {
12653
+ var _a;
12654
+ // Check if SDK is initialized - synchronous error
10360
12655
  if (!this.configManager.isInitialized()) {
10361
12656
  throw new CoBuyNotInitializedError();
10362
12657
  }
10363
- // Validate productId
12658
+ // Validate productId - synchronous error
10364
12659
  if (!options.productId) {
10365
12660
  const errorMessage = "productId is required for renderWidget";
10366
12661
  this.logger.error(errorMessage);
10367
- if (options.onError) {
10368
- options.onError(new Error(errorMessage));
10369
- }
10370
- return;
12662
+ const error = new Error(errorMessage);
12663
+ (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
12664
+ throw error;
10371
12665
  }
10372
12666
  try {
10373
12667
  const config = this.configManager.getConfig();
10374
12668
  const widget = new WidgetRoot(config, this.apiClient, this.analyticsClient);
10375
- widget.render(options);
12669
+ await widget.render(options);
10376
12670
  // Track widget instance so we can refresh it on important lifecycle events
10377
12671
  this.widgets.add(widget);
10378
12672
  }
10379
12673
  catch (error) {
10380
12674
  this.logger.error("Failed to render widget", error);
10381
12675
  if (options.onError) {
10382
- options.onError(error);
12676
+ options.onError(error instanceof Error ? error : new Error(String(error)));
10383
12677
  }
12678
+ throw error;
10384
12679
  }
10385
12680
  }
10386
12681
  /**
10387
- * Open modal for product details
12682
+ * Open a lobby modal for a group
12683
+ *
12684
+ * Displays a modal with group details, members, progress, and actions.
12685
+ * Only one modal is open at a time (others are automatically closed).
12686
+ *
12687
+ * @param {ModalOptions} options - Modal configuration
12688
+ * @param {string} options.productId - Product ID for the modal
12689
+ * @param {string} [options.groupId] - Group ID to display
12690
+ * @param {number} [options.groupNumber] - Group number for display
12691
+ * @param {string} [options.status] - Group status (e.g., 'pending', 'complete')
12692
+ * @param {number} [options.progress] - Group progress percentage (0-100)
12693
+ * @param {number} [options.currentMembers] - Current number of members
12694
+ * @param {number} [options.totalMembers] - Total members needed
12695
+ * @param {number} [options.timeLeft] - Seconds remaining before group closes
12696
+ * @param {number} [options.discount] - Discount percentage
12697
+ * @param {string} [options.groupLink] - Share link for the group
12698
+ * @param {Array} [options.activities] - Group activity feed
12699
+ * @param {boolean} [options.isLocked] - Whether group is locked
12700
+ * @param {Function} [options.onOpen] - Callback when modal opens
12701
+ * @param {Function} [options.onClose] - Callback when modal closes
12702
+ * @param {Function} [options.onCopyLink] - Callback when share link is copied
12703
+ * @param {Function} [options.onShare] - Callback when share action triggered
12704
+ *
12705
+ * @returns {void}
12706
+ *
12707
+ * @example
12708
+ * ```typescript
12709
+ * CoBuy.openModal({
12710
+ * productId: 'prod_abc123',
12711
+ * groupId: 'grp_xyz789',
12712
+ * status: 'pending',
12713
+ * progress: 75,
12714
+ * currentMembers: 3,
12715
+ * totalMembers: 4,
12716
+ * discount: 20,
12717
+ * onOpen: () => console.log('Modal opened'),
12718
+ * onClose: () => console.log('Modal closed')
12719
+ * });
12720
+ * ```
10388
12721
  */
10389
12722
  openModal(options) {
10390
12723
  var _a;
@@ -10439,7 +12772,7 @@ class CoBuy {
10439
12772
  },
10440
12773
  onCopyLink: options.onCopyLink,
10441
12774
  onShare: options.onShare,
10442
- }, this.apiClient, this.analyticsClient, this.socketManager, config.debug);
12775
+ }, this.apiClient, this.socketManager, config.debug);
10443
12776
  // Store in map for persistence
10444
12777
  this.modals.set(modalKey, modal);
10445
12778
  // Maintain backward compatibility
@@ -10471,7 +12804,17 @@ class CoBuy {
10471
12804
  }
10472
12805
  }
10473
12806
  /**
10474
- * Close the modal
12807
+ * Close the currently open modal
12808
+ *
12809
+ * Closes and cleans up the active lobby modal if one is open.
12810
+ * Safe to call even if no modal is currently open.
12811
+ *
12812
+ * @returns {void}
12813
+ *
12814
+ * @example
12815
+ * ```typescript
12816
+ * CoBuy.closeModal();
12817
+ * ```
10475
12818
  */
10476
12819
  closeModal() {
10477
12820
  try {
@@ -10531,6 +12874,9 @@ class CoBuy {
10531
12874
  this.logger.info(`Contact information set successfully (${contact.type})`);
10532
12875
  }
10533
12876
  else {
12877
+ if (this.handleFraudError(response.error, "setContact")) {
12878
+ return;
12879
+ }
10534
12880
  this.logger.error("Failed to set contact information", response.error);
10535
12881
  }
10536
12882
  }
@@ -10569,6 +12915,9 @@ class CoBuy {
10569
12915
  return response.data || null;
10570
12916
  }
10571
12917
  else {
12918
+ if (this.handleFraudError(response.error, "validateCheckout")) {
12919
+ return null;
12920
+ }
10572
12921
  this.logger.error("Failed to validate checkout", response.error);
10573
12922
  return null;
10574
12923
  }
@@ -10659,6 +13008,9 @@ class CoBuy {
10659
13008
  await Promise.all(refreshPromises.map((p) => p.then(() => undefined).catch(() => undefined)));
10660
13009
  }
10661
13010
  else {
13011
+ if (this.handleFraudError(response.error, "confirmCheckout")) {
13012
+ return;
13013
+ }
10662
13014
  this.logger.error("Failed to confirm checkout", response.error);
10663
13015
  }
10664
13016
  }
@@ -10668,30 +13020,91 @@ class CoBuy {
10668
13020
  }
10669
13021
  /**
10670
13022
  * Get the initialized API client instance
13023
+ *
13024
+ * Returns the API client used for making requests to the CoBuy backend.
13025
+ * Useful for advanced integrations or direct API calls.
13026
+ *
13027
+ * @returns {ApiClient | null} The API client instance, or null if SDK not initialized
13028
+ *
13029
+ * @example
13030
+ * ```typescript
13031
+ * const apiClient = CoBuy.getApiClient();
13032
+ * if (apiClient) {
13033
+ * const product = await apiClient.getProductReward('prod_abc123');
13034
+ * }
13035
+ * ```
10671
13036
  */
10672
13037
  getApiClient() {
10673
13038
  return this.apiClient;
10674
13039
  }
10675
13040
  /**
10676
13041
  * Get the initialized Analytics client instance
13042
+ *
13043
+ * Returns the analytics client used for tracking user interactions.
13044
+ * Useful for advanced event tracking or integration with analytics systems.
13045
+ *
13046
+ * @returns {AnalyticsClient | null} The analytics client instance, or null if SDK not initialized
13047
+ *
13048
+ * @example
13049
+ * ```typescript
13050
+ * const analytics = CoBuy.getAnalyticsClient();
13051
+ * if (analytics) {
13052
+ * await analytics.trackCustomEvent('user_action', { details: 'data' });
13053
+ * }
13054
+ * ```
10677
13055
  */
10678
13056
  getAnalyticsClient() {
10679
13057
  return this.analyticsClient;
10680
13058
  }
10681
13059
  /**
10682
13060
  * Get the initialized Socket manager instance
13061
+ *
13062
+ * Returns the socket manager used for real-time group updates.
13063
+ * Only available in public authentication mode.
13064
+ *
13065
+ * @returns {SocketManager | null} The socket manager instance, or null if not initialized (token mode doesn't use sockets)
13066
+ *
13067
+ * @example
13068
+ * ```typescript
13069
+ * const socketManager = CoBuy.getSocketManager();
13070
+ * if (socketManager) {
13071
+ * socketManager.bindHandlers({
13072
+ * onGroupCreated: (event) => console.log('New group:', event)
13073
+ * });
13074
+ * }
13075
+ * ```
10683
13076
  */
10684
13077
  getSocketManager() {
10685
13078
  return this.socketManager;
10686
13079
  }
10687
13080
  /**
10688
- * Get SDK version
13081
+ * Get the SDK version
13082
+ *
13083
+ * Returns the current version of the CoBuy SDK.
13084
+ *
13085
+ * @returns {string} SDK version in semver format (e.g., "1.0.0")
13086
+ *
13087
+ * @example
13088
+ * ```typescript
13089
+ * console.log(`CoBuy SDK v${CoBuy.version}`);
13090
+ * ```
10689
13091
  */
10690
13092
  get version() {
10691
13093
  return SDK_VERSION;
10692
13094
  }
10693
13095
  /**
10694
- * Clean up resources (sockets etc.) if needed by host app
13096
+ * Destroy the SDK and clean up resources
13097
+ *
13098
+ * Closes websocket connections, unbinds event handlers, and releases resources.
13099
+ * Call this when unloading the page or removing the SDK from use.
13100
+ *
13101
+ * @returns {void}
13102
+ *
13103
+ * @example
13104
+ * ```typescript
13105
+ * // When unmounting the SDK (e.g., React cleanup)
13106
+ * CoBuy.destroy();
13107
+ * ```
10695
13108
  */
10696
13109
  destroy() {
10697
13110
  try {
@@ -10707,17 +13120,19 @@ class CoBuy {
10707
13120
  }
10708
13121
  }
10709
13122
 
10710
- // Create singleton instance
10711
- const instance = new CoBuy();
10712
- // Attach to window if in browser environment
10713
- if (typeof window !== "undefined") {
10714
- if (window.CoBuy) {
10715
- console.warn("[CoBuy] Multiple SDK bundles detected. This may cause unexpected behavior.");
13123
+ // Create or reuse singleton instance
13124
+ const instance = (() => {
13125
+ if (typeof window === "undefined") {
13126
+ return new CoBuy();
10716
13127
  }
10717
- else {
10718
- window.CoBuy = instance;
13128
+ if (window.CoBuy) {
13129
+ console.warn("[CoBuy] Multiple SDK bundles detected. Reusing existing instance.");
13130
+ return window.CoBuy;
10719
13131
  }
10720
- }
13132
+ const created = new CoBuy();
13133
+ window.CoBuy = created;
13134
+ return created;
13135
+ })();
10721
13136
 
10722
13137
  export { PublicKeyAuth, TokenAuth, instance as default };
10723
13138
  //# sourceMappingURL=cobuy-sdk.esm.js.map