@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.
- package/README.md +453 -15
- package/dist/cobuy-sdk.esm.js +2655 -240
- package/dist/cobuy-sdk.esm.js.map +1 -1
- package/dist/cobuy-sdk.umd.js +2655 -240
- package/dist/cobuy-sdk.umd.js.map +1 -1
- package/dist/stats.html +4949 -0
- package/dist/types/core/analytics.d.ts +8 -4
- package/dist/types/core/api-client.d.ts +392 -38
- package/dist/types/core/auth-strategy.d.ts +7 -7
- package/dist/types/core/cobuy.d.ts +222 -10
- package/dist/types/core/config.d.ts +8 -1
- package/dist/types/core/errors.d.ts +108 -4
- package/dist/types/core/types.d.ts +230 -1
- package/dist/types/core/validation.d.ts +171 -0
- package/dist/types/index.d.ts +1 -2
- package/dist/types/ui/group-list/group-list-modal.d.ts +3 -3
- package/dist/types/ui/lobby/lobby-modal.d.ts +19 -11
- package/dist/types/ui/offline-redemption/offline-redemption-modal.d.ts +2 -2
- package/dist/types/ui/widget/theme.d.ts +127 -9
- package/dist/types/ui/widget/widget-root.d.ts +63 -9
- package/package.json +4 -1
package/dist/cobuy-sdk.esm.js
CHANGED
|
@@ -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
|
|
28
|
+
class CoBuyNotInitializedError extends CoBuyError {
|
|
5
29
|
constructor() {
|
|
6
|
-
super("CoBuy SDK
|
|
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
|
|
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
|
|
55
|
-
const
|
|
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
|
|
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
|
|
176
|
-
*
|
|
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
|
|
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: (
|
|
182
|
-
secondaryColor: (
|
|
183
|
-
textColor: (
|
|
184
|
-
borderRadius: (
|
|
185
|
-
fontFamily: (
|
|
186
|
-
animationSpeed: (
|
|
187
|
-
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
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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.
|
|
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
|
-
//
|
|
3024
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
3940
|
-
root.
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 = `
|
|
4896
|
+
const errorMessage = `Container not found for selector: ${selector}`;
|
|
4172
4897
|
this.logger.error(errorMessage);
|
|
4173
|
-
|
|
4174
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
4375
|
-
|
|
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
|
-
//
|
|
4590
|
-
if (
|
|
4591
|
-
this.
|
|
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 = "
|
|
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 = "
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 },
|
|
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
|
-
|
|
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(
|
|
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:
|
|
5288
|
-
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
|
|
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
|
|
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
|
|
5396
|
-
*
|
|
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
|
-
* @
|
|
5399
|
-
*
|
|
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
|
|
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
|
-
* @
|
|
5448
|
-
*
|
|
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
|
|
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
|
|
5498
|
-
* @
|
|
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
|
-
*
|
|
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
|
-
*
|
|
5559
|
-
*
|
|
5560
|
-
*
|
|
5561
|
-
*
|
|
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
|
-
*
|
|
5564
|
-
*
|
|
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
|
-
*
|
|
5594
|
-
*
|
|
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
|
-
* @
|
|
5597
|
-
*
|
|
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
|
-
*
|
|
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
|
|
5628
|
-
* @param checkoutRef - The checkout reference
|
|
5629
|
-
* @returns Promise with
|
|
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
|
|
5657
|
-
*
|
|
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
|
-
* @
|
|
5660
|
-
*
|
|
5661
|
-
*
|
|
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
|
-
|
|
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: (
|
|
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
|
-
//
|
|
5760
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
11444
|
+
|
|
11445
|
+
var version = "2.0.0";
|
|
11446
|
+
|
|
10109
11447
|
/**
|
|
10110
|
-
*
|
|
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
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10368
|
-
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
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 =
|
|
10712
|
-
|
|
10713
|
-
|
|
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
|
-
|
|
10718
|
-
|
|
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
|