@cshah18/sdk 1.0.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.
@@ -0,0 +1,1226 @@
1
+ /**
2
+ * Thrown when the SDK is used before initialization
3
+ */
4
+ class CoBuyNotInitializedError extends Error {
5
+ constructor() {
6
+ super("CoBuy SDK has not been initialized. Call CoBuy.init(options) first.");
7
+ this.name = "CoBuyNotInitializedError";
8
+ Object.setPrototypeOf(this, CoBuyNotInitializedError.prototype);
9
+ }
10
+ }
11
+ /**
12
+ * Thrown when invalid configuration is provided
13
+ */
14
+ class CoBuyInvalidConfigError extends Error {
15
+ constructor(message) {
16
+ super(`Invalid CoBuy configuration: ${message}`);
17
+ this.name = "CoBuyInvalidConfigError";
18
+ Object.setPrototypeOf(this, CoBuyInvalidConfigError.prototype);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Manages SDK configuration with validation and defaults
24
+ */
25
+ class ConfigManager {
26
+ constructor() {
27
+ this.config = null;
28
+ }
29
+ /**
30
+ * Set and validate SDK configuration
31
+ */
32
+ setConfig(options) {
33
+ // Validate merchantKey
34
+ if (!options.merchantKey || typeof options.merchantKey !== "string") {
35
+ throw new CoBuyInvalidConfigError("merchantKey is required and must be a string");
36
+ }
37
+ // Set environment with default
38
+ const env = options.env || "production";
39
+ // Set API base URL based on environment (computed internally)
40
+ const apiBaseUrl = ConfigManager.DEFAULT_API_URLS[env];
41
+ // Theme configuration
42
+ const theme = options.theme || {};
43
+ // Debug flag
44
+ const debug = options.debug || false;
45
+ // Create and store internal config
46
+ this.config = {
47
+ merchantKey: options.merchantKey,
48
+ env,
49
+ apiBaseUrl,
50
+ theme,
51
+ debug,
52
+ events: options.events,
53
+ };
54
+ return this.config;
55
+ }
56
+ /**
57
+ * Get current configuration
58
+ * @throws CoBuyNotInitializedError if SDK has not been initialized
59
+ */
60
+ getConfig() {
61
+ if (!this.config) {
62
+ throw new Error("CoBuy SDK has not been initialized. Call CoBuy.init(options) first.");
63
+ }
64
+ return this.config;
65
+ }
66
+ /**
67
+ * Check if SDK has been initialized
68
+ */
69
+ isInitialized() {
70
+ return this.config !== null;
71
+ }
72
+ }
73
+ /**
74
+ * Default API base URLs for each environment
75
+ */
76
+ ConfigManager.DEFAULT_API_URLS = {
77
+ production: "https://api.cobuyza.co.za/api",
78
+ local: "http://localhost:9000/api",
79
+ };
80
+
81
+ /**
82
+ * Logger utility for CoBuy SDK with optional debug mode
83
+ */
84
+ class Logger {
85
+ constructor(debug = false) {
86
+ this.debugEnabled = debug;
87
+ }
88
+ /**
89
+ * Log info level messages (only shown in debug mode)
90
+ */
91
+ info(message, ...args) {
92
+ if (this.debugEnabled) {
93
+ console.log(`${Logger.PREFIX} ${message}`, ...args);
94
+ }
95
+ }
96
+ /**
97
+ * Log debug level messages (only shown in debug mode)
98
+ */
99
+ debug(message, ...args) {
100
+ if (this.debugEnabled) {
101
+ console.debug(`${Logger.PREFIX} ${message}`, ...args);
102
+ }
103
+ }
104
+ /**
105
+ * Log warning level messages
106
+ */
107
+ warn(message, ...args) {
108
+ console.warn(`${Logger.PREFIX} ${message}`, ...args);
109
+ }
110
+ /**
111
+ * Log error level messages
112
+ */
113
+ error(message, ...args) {
114
+ console.error(`${Logger.PREFIX} ${message}`, ...args);
115
+ }
116
+ /**
117
+ * Update debug flag
118
+ */
119
+ setDebug(debug) {
120
+ this.debugEnabled = debug;
121
+ }
122
+ }
123
+ Logger.PREFIX = "[CoBuy]";
124
+
125
+ /**
126
+ * API endpoint paths
127
+ *
128
+ * NOTE: These endpoints are planned for future implementation in the CoBuy backend.
129
+ * The SDK is structured to integrate with these endpoints once they become available.
130
+ * Until then, API calls to these endpoints will fail gracefully without crashing the application.
131
+ *
132
+ * Expected backend implementation timeline: TBD
133
+ */
134
+ const API_ENDPOINTS = {
135
+ // Product endpoints (Active)
136
+ PRODUCT_DETAILS: "/v1/products/:productId",
137
+ PRODUCT_AVAILABILITY: "/v1/products/:productId/availability",
138
+ PRODUCT_REWARD: "/v1/sdk/products/:productId/reward", // ✅ Implemented
139
+ // Group endpoints (Future implementation)
140
+ GROUP_CREATE: "/v1/groups",
141
+ GROUP_JOIN: "/v1/groups/:groupId/join",
142
+ GROUP_DETAILS: "/v1/groups/:groupId",
143
+ GROUP_MEMBERS: "/v1/groups/:groupId/members",
144
+ // Checkout endpoints (Future implementation)
145
+ CHECKOUT_INIT: "/v1/checkout/init",
146
+ CHECKOUT_STATUS: "/v1/checkout/:checkoutId/status",
147
+ // Widget endpoints (Future implementation - reserved for modal/group lobby features)
148
+ // WIDGET_CONFIG: Controls display settings, group rules, feature flags, and theming
149
+ // Will be activated when implementing modal group lobby, social sharing, and advanced customization
150
+ WIDGET_CONFIG: "/v1/widget/config",
151
+ WIDGET_ANALYTICS: "/v1/widget/analytics", // ✅ Implemented
152
+ };
153
+ /**
154
+ * Build full API URL from base URL and endpoint path
155
+ */
156
+ function buildApiUrl(baseUrl, endpoint, params) {
157
+ let url = endpoint;
158
+ // Replace path parameters
159
+ if (params) {
160
+ Object.entries(params).forEach(([key, value]) => {
161
+ url = url.replace(`:${key}`, encodeURIComponent(value));
162
+ });
163
+ }
164
+ return `${baseUrl}${url}`;
165
+ }
166
+
167
+ /**
168
+ * Default theme values for the CoBuy widget
169
+ * These provide a consistent baseline if merchants don't specify custom colors
170
+ */
171
+ const DEFAULT_THEME = {
172
+ primaryColor: "#4f46e5", // Indigo-600
173
+ secondaryColor: "#6366f1", // Indigo-500
174
+ textColor: "#111827", // Gray-900
175
+ borderRadius: "8px",
176
+ fontFamily: "Inter, system-ui, -apple-system, sans-serif",
177
+ animationSpeed: "normal",
178
+ animationStyle: "subtle",
179
+ };
180
+ /**
181
+ * Animation duration mappings (in milliseconds)
182
+ */
183
+ const ANIMATION_DURATIONS = {
184
+ none: 0,
185
+ fast: 150,
186
+ normal: 300,
187
+ slow: 500,
188
+ };
189
+ /**
190
+ * Animation easing and transform mappings
191
+ */
192
+ const ANIMATION_STYLES = {
193
+ none: { easing: "linear", transform: "translateY(0)" },
194
+ subtle: { easing: "ease-out", transform: "translateY(0)" }, // Fade only
195
+ smooth: { easing: "ease-out", transform: "translateY(8px)" }, // Fade + slide
196
+ bouncy: { easing: "cubic-bezier(0.68, -0.55, 0.265, 1.55)", transform: "translateY(12px)" }, // Spring
197
+ };
198
+ /**
199
+ * Merge global theme with optional widget-specific overrides
200
+ * Priority: widgetTheme > globalTheme > defaults
201
+ */
202
+ function mergeTheme(globalTheme = {}, widgetTheme = {}) {
203
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
204
+ return {
205
+ primaryColor: (_b = (_a = widgetTheme.primaryColor) !== null && _a !== void 0 ? _a : globalTheme.primaryColor) !== null && _b !== void 0 ? _b : DEFAULT_THEME.primaryColor,
206
+ secondaryColor: (_d = (_c = widgetTheme.secondaryColor) !== null && _c !== void 0 ? _c : globalTheme.secondaryColor) !== null && _d !== void 0 ? _d : DEFAULT_THEME.secondaryColor,
207
+ textColor: (_f = (_e = widgetTheme.textColor) !== null && _e !== void 0 ? _e : globalTheme.textColor) !== null && _f !== void 0 ? _f : DEFAULT_THEME.textColor,
208
+ borderRadius: (_h = (_g = widgetTheme.borderRadius) !== null && _g !== void 0 ? _g : globalTheme.borderRadius) !== null && _h !== void 0 ? _h : DEFAULT_THEME.borderRadius,
209
+ fontFamily: (_k = (_j = widgetTheme.fontFamily) !== null && _j !== void 0 ? _j : globalTheme.fontFamily) !== null && _k !== void 0 ? _k : DEFAULT_THEME.fontFamily,
210
+ animationSpeed: (_m = (_l = widgetTheme.animationSpeed) !== null && _l !== void 0 ? _l : globalTheme.animationSpeed) !== null && _m !== void 0 ? _m : DEFAULT_THEME.animationSpeed,
211
+ animationStyle: (_p = (_o = widgetTheme.animationStyle) !== null && _o !== void 0 ? _o : globalTheme.animationStyle) !== null && _p !== void 0 ? _p : DEFAULT_THEME.animationStyle,
212
+ };
213
+ }
214
+ /**
215
+ * Apply theme to widget container using scoped CSS custom properties
216
+ * This approach ensures:
217
+ * 1. Styles are isolated to this widget instance
218
+ * 2. No conflicts with merchant's global styles
219
+ * 3. Easy runtime theme updates
220
+ * 4. Browser handles invalid values gracefully
221
+ */
222
+ function applyTheme(container, theme) {
223
+ // Apply color and style CSS variables
224
+ container.style.setProperty("--cobuy-primary-color", theme.primaryColor);
225
+ container.style.setProperty("--cobuy-secondary-color", theme.secondaryColor);
226
+ container.style.setProperty("--cobuy-text-color", theme.textColor);
227
+ container.style.setProperty("--cobuy-border-radius", theme.borderRadius);
228
+ container.style.setProperty("--cobuy-font-family", theme.fontFamily);
229
+ // Apply animation CSS variables
230
+ const duration = ANIMATION_DURATIONS[theme.animationSpeed];
231
+ const style = ANIMATION_STYLES[theme.animationStyle];
232
+ container.style.setProperty("--cobuy-animation-duration", `${duration}ms`);
233
+ container.style.setProperty("--cobuy-animation-easing", style.easing);
234
+ container.style.setProperty("--cobuy-animation-transform", style.transform);
235
+ }
236
+
237
+ /**
238
+ * Debounce utility to prevent rapid function calls
239
+ * @param func Function to debounce
240
+ * @param delay Time in milliseconds to wait before executing
241
+ * @returns Debounced function
242
+ */
243
+ function debounce(func, delay) {
244
+ let timeoutId = null;
245
+ return function debounced(...args) {
246
+ if (timeoutId !== null) {
247
+ clearTimeout(timeoutId);
248
+ }
249
+ const argsToPass = args;
250
+ timeoutId = setTimeout(() => {
251
+ func(...argsToPass);
252
+ timeoutId = null;
253
+ }, delay);
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Widget state enumeration for tracking render lifecycle
259
+ */
260
+ var WidgetState;
261
+ (function (WidgetState) {
262
+ WidgetState["LOADING"] = "loading";
263
+ WidgetState["LOADED"] = "loaded";
264
+ WidgetState["ERROR"] = "error";
265
+ })(WidgetState || (WidgetState = {}));
266
+ /**
267
+ * WidgetRoot handles rendering of the CoBuy widget into a DOM container
268
+ */
269
+ class WidgetRoot {
270
+ constructor(config, apiClient, analyticsClient = null) {
271
+ this.state = WidgetState.LOADING;
272
+ this.retryCount = 0;
273
+ this.MAX_RETRIES = 1;
274
+ this.currentProductId = null;
275
+ this.currentContainer = null;
276
+ this.debouncedCTAClick = null;
277
+ this.logger = new Logger(config.debug);
278
+ this.apiClient = apiClient;
279
+ this.analyticsClient = analyticsClient;
280
+ this.config = config;
281
+ this.events = config.events;
282
+ // Initialize debounced CTA click handler
283
+ this.debouncedCTAClick = debounce((productId) => {
284
+ this.handleCTAClick(productId);
285
+ }, 300); // 300ms debounce to prevent double-clicks
286
+ }
287
+ /**
288
+ * Render the widget into the specified container
289
+ */
290
+ async render(options) {
291
+ var _a, _b, _c, _d, _e;
292
+ try {
293
+ // Resolve container
294
+ const container = this.resolveContainer(options.container);
295
+ if (!container) {
296
+ const selector = typeof options.container === "string" ? options.container : "<HTMLElement>";
297
+ const errorMessage = `CoBuy: container not found for ${selector}`;
298
+ this.logger.error(errorMessage);
299
+ const error = new Error(errorMessage);
300
+ (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, error);
301
+ throw error;
302
+ }
303
+ // Host safety + idempotency markers
304
+ container.setAttribute("data-cobuy-mounted", "true");
305
+ container.style.maxWidth = "100%";
306
+ container.style.width = "100%";
307
+ container.style.boxSizing = "border-box";
308
+ container.style.setProperty("--cobuy-gap", "8px");
309
+ container.style.setProperty("--cobuy-cta-min-height", "44px");
310
+ container.style.setProperty("--cobuy-reward-font-size", "14px");
311
+ this.injectBaseStyles();
312
+ // Apply theme (merge global + widget-specific overrides)
313
+ const theme = mergeTheme(this.config.theme, options.theme);
314
+ applyTheme(container, theme);
315
+ this.logger.info("Theme applied to widget", theme);
316
+ // Store container and productId for retry functionality
317
+ this.currentContainer = container;
318
+ this.currentProductId = options.productId;
319
+ // Reset state and retry count
320
+ this.state = WidgetState.LOADING;
321
+ this.retryCount = 0;
322
+ // Emit loading event
323
+ (_c = (_b = this.events) === null || _b === void 0 ? void 0 : _b.onLoading) === null || _c === void 0 ? void 0 : _c.call(_b, options.productId);
324
+ // Render skeleton loading state
325
+ this.renderSkeleton(container);
326
+ // Fetch widget configuration and reward data if API client is available
327
+ let rewardData = null;
328
+ if (this.apiClient) {
329
+ await this.fetchWidgetConfig(options.productId);
330
+ rewardData = await this.fetchRewardWithRetry(options.productId);
331
+ }
332
+ else {
333
+ this.state = WidgetState.LOADED;
334
+ }
335
+ // Render widget or error based on final state
336
+ // Note: state may be ERROR after fetchRewardWithRetry despite TypeScript's flow analysis
337
+ if (this.state === WidgetState.ERROR) {
338
+ this.renderError(container);
339
+ }
340
+ else {
341
+ // LOADED state - render widget (even if rewardData is null, createWidget handles it)
342
+ this.createWidget(rewardData, container);
343
+ this.logger.info(`Widget rendered for product: ${options.productId}`);
344
+ }
345
+ }
346
+ catch (error) {
347
+ this.logger.error("Failed to render widget", error);
348
+ this.state = WidgetState.ERROR;
349
+ (_e = (_d = this.events) === null || _d === void 0 ? void 0 : _d.onError) === null || _e === void 0 ? void 0 : _e.call(_d, options.productId, error);
350
+ if (options.onError) {
351
+ options.onError(error);
352
+ }
353
+ if (this.currentContainer) {
354
+ this.renderError(this.currentContainer);
355
+ }
356
+ }
357
+ }
358
+ /**
359
+ * Fetch reward data with automatic retry on failure
360
+ */
361
+ async fetchRewardWithRetry(productId) {
362
+ var _a, _b, _c, _d;
363
+ try {
364
+ const reward = await this.fetchReward(productId);
365
+ this.state = WidgetState.LOADED;
366
+ this.logger.info("Reward data fetched successfully");
367
+ (_b = (_a = this.events) === null || _a === void 0 ? void 0 : _a.onLoaded) === null || _b === void 0 ? void 0 : _b.call(_a, productId, reward);
368
+ return reward;
369
+ }
370
+ catch (error) {
371
+ if (this.retryCount < this.MAX_RETRIES) {
372
+ this.retryCount++;
373
+ this.logger.info(`Retrying reward fetch (${this.retryCount}/${this.MAX_RETRIES})`, error);
374
+ // Immediate retry
375
+ return this.fetchRewardWithRetry(productId);
376
+ }
377
+ this.state = WidgetState.ERROR;
378
+ this.logger.error("Failed to fetch reward after retries", error);
379
+ (_d = (_c = this.events) === null || _c === void 0 ? void 0 : _c.onError) === null || _d === void 0 ? void 0 : _d.call(_c, productId, error);
380
+ return null;
381
+ }
382
+ }
383
+ /**
384
+ * Fetch reward data for a product
385
+ */
386
+ async fetchReward(productId) {
387
+ var _a;
388
+ if (!this.apiClient) {
389
+ return null;
390
+ }
391
+ const response = await this.apiClient.getProductReward(productId);
392
+ if (response.success && response.data) {
393
+ this.logger.info("Reward data loaded", response.data);
394
+ return response.data;
395
+ }
396
+ // API call failed or returned no data - throw error to trigger retry
397
+ const errorMessage = ((_a = response.error) === null || _a === void 0 ? void 0 : _a.message) || "Reward data unavailable";
398
+ this.logger.warn(errorMessage, response.error);
399
+ throw new Error(errorMessage);
400
+ }
401
+ /**
402
+ * Fetch widget configuration from API
403
+ *
404
+ * NOTE: This endpoint is currently NOT USED in the SDK implementation.
405
+ * It is reserved for future enhancements such as:
406
+ * - Server-side control of widget display settings (show/hide elements)
407
+ * - Per-merchant feature flags (enable/disable social sharing, group creation)
408
+ * - Group size requirements and timeout configurations
409
+ * - Modal layout and behavior customization
410
+ * - A/B testing and dynamic theming
411
+ *
412
+ * For now, widget behavior is controlled by:
413
+ * 1. Merchant's init() options (theme, debug)
414
+ * 2. Reward data from /v1/sdk/products/:productId/reward
415
+ *
416
+ * This method is kept for future integration when modal group lobby,
417
+ * social sharing, and advanced customization features are implemented.
418
+ *
419
+ * @future Will be activated in modal implementation (post-SCRUM-248)
420
+ */
421
+ async fetchWidgetConfig(productId) {
422
+ if (!this.apiClient) {
423
+ return;
424
+ }
425
+ try {
426
+ const response = await this.apiClient.get(API_ENDPOINTS.WIDGET_CONFIG + `?productId=${productId}`);
427
+ if (response.success) {
428
+ this.logger.info("Widget configuration loaded", response.data);
429
+ // TODO: Use configuration data to customize widget appearance and behavior
430
+ }
431
+ else {
432
+ // Gracefully handle API errors - widget will still render with defaults
433
+ this.logger.warn("Widget configuration not available (backend endpoint pending implementation)", response.error);
434
+ }
435
+ }
436
+ catch (error) {
437
+ // Catch any network errors and continue - widget rendering should not fail
438
+ this.logger.warn("Could not fetch widget configuration (backend endpoint pending)", error);
439
+ }
440
+ }
441
+ /**
442
+ * Resolve container from string selector or HTMLElement
443
+ */
444
+ resolveContainer(container) {
445
+ if (typeof container === "string") {
446
+ return document.querySelector(container);
447
+ }
448
+ return container || null;
449
+ }
450
+ /**
451
+ * Render skeleton loading state with animated shimmer effect
452
+ */
453
+ renderSkeleton(container) {
454
+ // Inject animations once
455
+ const shimmerStyle = document.createElement("style");
456
+ shimmerStyle.textContent = `
457
+ @keyframes cobuy-shimmer {
458
+ 0% { background-position: -200px 0; }
459
+ 100% { background-position: calc(200px + 100%) 0; }
460
+ }
461
+ @keyframes cobuy-fadeIn {
462
+ from {
463
+ opacity: 0;
464
+ transform: var(--cobuy-animation-transform, translateY(8px));
465
+ }
466
+ to {
467
+ opacity: 1;
468
+ transform: translateY(0);
469
+ }
470
+ }
471
+ .cobuy-skeleton-text,
472
+ .cobuy-skeleton-button {
473
+ animation: cobuy-shimmer 1.5s ease-in-out infinite;
474
+ background: linear-gradient(90deg, #f0f0f0 0px, #e0e0e0 40px, #f0f0f0 80px);
475
+ background-size: 200px 100%;
476
+ }
477
+ `;
478
+ if (!document.querySelector("#cobuy-shimmer-style")) {
479
+ shimmerStyle.id = "cobuy-shimmer-style";
480
+ document.head.appendChild(shimmerStyle);
481
+ }
482
+ // Reward text placeholder - render directly with accessibility
483
+ const textPlaceholder = document.createElement("div");
484
+ textPlaceholder.className = "cobuy-skeleton-text";
485
+ textPlaceholder.setAttribute("aria-busy", "true");
486
+ textPlaceholder.setAttribute("role", "status");
487
+ textPlaceholder.setAttribute("aria-label", "Loading reward information");
488
+ textPlaceholder.style.height = "18px";
489
+ textPlaceholder.style.width = "70%";
490
+ textPlaceholder.style.borderRadius = "4px";
491
+ textPlaceholder.style.marginBottom = "8px";
492
+ textPlaceholder.style.opacity = "0";
493
+ textPlaceholder.style.animation =
494
+ "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards";
495
+ // Button placeholder - render directly with accessibility
496
+ const buttonPlaceholder = document.createElement("div");
497
+ buttonPlaceholder.className = "cobuy-skeleton-button";
498
+ buttonPlaceholder.setAttribute("aria-busy", "true");
499
+ buttonPlaceholder.setAttribute("role", "button");
500
+ buttonPlaceholder.setAttribute("aria-label", "Loading action button");
501
+ buttonPlaceholder.style.height = "var(--cobuy-cta-min-height, 44px)";
502
+ buttonPlaceholder.style.minHeight = "var(--cobuy-cta-min-height, 44px)";
503
+ buttonPlaceholder.style.width = "100%";
504
+ buttonPlaceholder.style.borderRadius = "6px";
505
+ buttonPlaceholder.style.opacity = "0";
506
+ buttonPlaceholder.style.animation =
507
+ "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards 100ms";
508
+ container.innerHTML = "";
509
+ container.appendChild(textPlaceholder);
510
+ container.appendChild(buttonPlaceholder);
511
+ }
512
+ /**
513
+ * Render error state with retry button
514
+ */
515
+ renderError(container) {
516
+ // Error message - render directly with accessibility
517
+ const message = document.createElement("div");
518
+ message.className = "cobuy-error-message";
519
+ message.setAttribute("role", "alert");
520
+ message.setAttribute("aria-live", "polite");
521
+ message.setAttribute("aria-label", "Error loading CoBuy offer");
522
+ message.style.fontSize = "13px";
523
+ message.style.color = "#991b1b";
524
+ message.style.fontFamily =
525
+ "var(--cobuy-font-family, Inter, system-ui, -apple-system, sans-serif)";
526
+ message.style.marginBottom = "12px";
527
+ message.style.lineHeight = "1.4";
528
+ message.style.wordWrap = "break-word";
529
+ message.style.opacity = "0";
530
+ message.style.animation =
531
+ "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards";
532
+ message.textContent = "Unable to load CoBuy offer. Please try again.";
533
+ // Retry button - render directly with accessibility
534
+ const retryButton = document.createElement("button");
535
+ retryButton.type = "button";
536
+ retryButton.className = "cobuy-error-button";
537
+ retryButton.textContent = "Retry";
538
+ retryButton.setAttribute("aria-label", "Retry loading CoBuy offer");
539
+ retryButton.setAttribute("role", "button");
540
+ retryButton.setAttribute("tabindex", "0");
541
+ retryButton.title = "Click to retry loading the CoBuy offer";
542
+ retryButton.style.padding = "10px 14px";
543
+ retryButton.style.minHeight = "36px";
544
+ retryButton.style.backgroundColor = "#dc2626";
545
+ retryButton.style.color = "white";
546
+ retryButton.style.border = "none";
547
+ retryButton.style.borderRadius = "var(--cobuy-border-radius, 6px)";
548
+ retryButton.style.fontSize = "13px";
549
+ retryButton.style.fontWeight = "600";
550
+ retryButton.style.fontFamily =
551
+ "var(--cobuy-font-family, Inter, system-ui, -apple-system, sans-serif)";
552
+ retryButton.style.cursor = "pointer";
553
+ retryButton.style.transition = "background-color 0.2s, transform 0.1s";
554
+ retryButton.style.opacity = "0";
555
+ retryButton.style.animation =
556
+ "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards 100ms";
557
+ retryButton.addEventListener("mouseenter", () => {
558
+ retryButton.style.backgroundColor = "#b91c1c";
559
+ });
560
+ retryButton.addEventListener("mouseleave", () => {
561
+ retryButton.style.backgroundColor = "#dc2626";
562
+ });
563
+ retryButton.addEventListener("mousedown", () => {
564
+ retryButton.style.transform = "scale(0.97)";
565
+ });
566
+ retryButton.addEventListener("mouseup", () => {
567
+ retryButton.style.transform = "scale(1)";
568
+ });
569
+ retryButton.addEventListener("click", () => this.handleRetry());
570
+ container.innerHTML = "";
571
+ container.appendChild(message);
572
+ container.appendChild(retryButton);
573
+ }
574
+ /**
575
+ * Handle retry button click - reset retry count and re-fetch
576
+ */
577
+ async handleRetry() {
578
+ var _a, _b;
579
+ if (!this.currentContainer || !this.currentProductId) {
580
+ this.logger.error("Cannot retry: missing container or productId");
581
+ return;
582
+ }
583
+ this.retryCount = 0;
584
+ this.state = WidgetState.LOADING;
585
+ (_b = (_a = this.events) === null || _a === void 0 ? void 0 : _a.onLoading) === null || _b === void 0 ? void 0 : _b.call(_a, this.currentProductId);
586
+ this.renderSkeleton(this.currentContainer);
587
+ const rewardData = await this.fetchRewardWithRetry(this.currentProductId);
588
+ // Render based on final state (state may be ERROR after fetchRewardWithRetry)
589
+ if (this.state === WidgetState.ERROR) {
590
+ this.renderError(this.currentContainer);
591
+ }
592
+ else {
593
+ // LOADED state
594
+ this.createWidget(rewardData, this.currentContainer);
595
+ }
596
+ }
597
+ createWidget(rewardData, container) {
598
+ var _a, _b;
599
+ const wrapper = document.createElement("div");
600
+ wrapper.className = "cobuy-widget";
601
+ wrapper.style.display = "grid";
602
+ wrapper.style.gridTemplateColumns = "1fr";
603
+ wrapper.style.gap = "var(--cobuy-gap, 8px)";
604
+ wrapper.style.alignItems = "stretch";
605
+ wrapper.style.width = "100%";
606
+ wrapper.style.maxWidth = "100%";
607
+ wrapper.style.boxSizing = "border-box";
608
+ // Reward text - use semantic <p> tag with accessibility attributes
609
+ const rewardLine = document.createElement("p");
610
+ rewardLine.className = "cobuy-reward-text";
611
+ rewardLine.setAttribute("role", "region");
612
+ rewardLine.setAttribute("aria-label", "CoBuy reward offer");
613
+ rewardLine.style.fontSize = "var(--cobuy-reward-font-size, 14px)";
614
+ rewardLine.style.fontWeight = "600";
615
+ rewardLine.style.color = "var(--cobuy-text-color, #111827)";
616
+ rewardLine.style.fontFamily =
617
+ "var(--cobuy-font-family, Inter, system-ui, -apple-system, sans-serif)";
618
+ rewardLine.style.margin = "0";
619
+ rewardLine.style.lineHeight = "1.4";
620
+ rewardLine.style.wordWrap = "break-word";
621
+ rewardLine.style.overflowWrap = "break-word";
622
+ rewardLine.style.whiteSpace = "normal";
623
+ rewardLine.style.width = "100%";
624
+ rewardLine.style.opacity = "0";
625
+ rewardLine.style.animation =
626
+ "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards";
627
+ if (((_a = rewardData === null || rewardData === void 0 ? void 0 : rewardData.eligibility) === null || _a === void 0 ? void 0 : _a.isEligible) && rewardData.reward) {
628
+ const rewardText = this.formatRewardText(rewardData.reward);
629
+ rewardLine.textContent = rewardText
630
+ ? `Save up to ${rewardText} with CoBuy`
631
+ : "CoBuy reward available";
632
+ rewardLine.setAttribute("aria-label", `Eligible for CoBuy reward: ${rewardLine.textContent}`);
633
+ rewardLine.title = rewardLine.textContent;
634
+ }
635
+ else if (rewardData && !((_b = rewardData.eligibility) === null || _b === void 0 ? void 0 : _b.isEligible)) {
636
+ rewardLine.textContent = "Join with CoBuy to unlock rewards";
637
+ rewardLine.setAttribute("aria-label", "Join with CoBuy to unlock rewards");
638
+ rewardLine.title = "Join with CoBuy to unlock rewards";
639
+ rewardLine.style.color = "#6b7280";
640
+ }
641
+ else {
642
+ rewardLine.textContent = "CoBuy offer loading or unavailable";
643
+ rewardLine.setAttribute("aria-label", "CoBuy offer loading or unavailable");
644
+ rewardLine.title = "CoBuy offer loading or unavailable";
645
+ rewardLine.style.color = "#6b7280";
646
+ }
647
+ // Button - semantic button element with accessibility
648
+ const button = document.createElement("button");
649
+ button.type = "button";
650
+ button.className = "cobuy-button";
651
+ button.textContent = "Buy with CoBuy";
652
+ button.setAttribute("aria-label", "Buy this product with CoBuy for group buying discounts");
653
+ button.setAttribute("role", "button");
654
+ button.setAttribute("tabindex", "0");
655
+ button.title = "Buy with CoBuy for discounts when others join";
656
+ button.style.padding = "10px 14px";
657
+ button.style.backgroundColor = "var(--cobuy-primary-color, #4f46e5)";
658
+ button.style.color = "var(--cobuy-button-text-color, white)";
659
+ button.style.border = "none";
660
+ button.style.borderRadius = "var(--cobuy-border-radius, 6px)";
661
+ button.style.fontSize = "13px";
662
+ button.style.fontWeight = "700";
663
+ button.style.fontFamily =
664
+ "var(--cobuy-font-family, Inter, system-ui, -apple-system, sans-serif)";
665
+ button.style.cursor = "pointer";
666
+ button.style.width = "100%";
667
+ button.style.minHeight = "var(--cobuy-cta-min-height, 44px)";
668
+ button.style.height = "auto";
669
+ button.style.lineHeight = "1.4";
670
+ button.style.wordWrap = "break-word";
671
+ button.style.whiteSpace = "normal";
672
+ button.style.transition = "opacity 0.2s, transform 0.1s";
673
+ button.style.opacity = "0";
674
+ button.style.animation =
675
+ "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards 100ms";
676
+ // Hover effect using opacity for better theme compatibility
677
+ button.addEventListener("mouseenter", () => {
678
+ button.style.opacity = "0.9";
679
+ });
680
+ button.addEventListener("mouseleave", () => {
681
+ button.style.opacity = "1";
682
+ });
683
+ button.addEventListener("mousedown", () => {
684
+ button.style.transform = "scale(0.97)";
685
+ });
686
+ button.addEventListener("mouseup", () => {
687
+ button.style.transform = "scale(1)";
688
+ });
689
+ // Emit button click event
690
+ button.addEventListener("click", () => {
691
+ if (this.currentProductId && this.debouncedCTAClick) {
692
+ this.debouncedCTAClick(this.currentProductId);
693
+ }
694
+ });
695
+ wrapper.appendChild(rewardLine);
696
+ wrapper.appendChild(button);
697
+ container.innerHTML = "";
698
+ container.appendChild(wrapper);
699
+ }
700
+ /**
701
+ * Inject base layout styles for responsive widget rendering
702
+ */
703
+ injectBaseStyles() {
704
+ if (document.querySelector("#cobuy-widget-base-styles")) {
705
+ return;
706
+ }
707
+ const style = document.createElement("style");
708
+ style.id = "cobuy-widget-base-styles";
709
+ style.textContent = `
710
+ .cobuy-widget {
711
+ display: grid;
712
+ grid-template-columns: 1fr;
713
+ gap: var(--cobuy-gap, 8px);
714
+ align-items: stretch;
715
+ width: 100%;
716
+ }
717
+
718
+ .cobuy-widget .cobuy-reward-text {
719
+ margin: 0;
720
+ width: 100%;
721
+ }
722
+
723
+ .cobuy-widget .cobuy-button {
724
+ min-height: var(--cobuy-cta-min-height, 44px);
725
+ width: 100%;
726
+ }
727
+
728
+ @media (max-width: 640px) {
729
+ .cobuy-widget {
730
+ grid-template-columns: 1fr;
731
+ }
732
+ }
733
+ `;
734
+ document.head.appendChild(style);
735
+ }
736
+ /**
737
+ * Handle CTA button click with analytics and modal opening
738
+ */
739
+ handleCTAClick(productId) {
740
+ var _a, _b;
741
+ this.logger.info(`CTA clicked for product: ${productId}`);
742
+ // Track analytics event asynchronously (fire-and-forget)
743
+ if (this.analyticsClient) {
744
+ this.analyticsClient.trackCtaClick(productId).catch((error) => {
745
+ this.logger.warn("Analytics tracking failed", error);
746
+ });
747
+ }
748
+ // Emit button click event
749
+ (_b = (_a = this.events) === null || _a === void 0 ? void 0 : _a.onButtonClick) === null || _b === void 0 ? void 0 : _b.call(_a, productId);
750
+ // Open modal if SDK instance is available
751
+ // Access the global CoBuy SDK instance through window
752
+ if (typeof window !== "undefined") {
753
+ const cobuySDK = window.CoBuy;
754
+ if (cobuySDK) {
755
+ try {
756
+ cobuySDK.openModal({
757
+ productId,
758
+ });
759
+ }
760
+ catch (error) {
761
+ this.logger.warn("Failed to open modal", error);
762
+ }
763
+ }
764
+ }
765
+ }
766
+ formatRewardText(reward) {
767
+ var _a;
768
+ if (!reward)
769
+ return "";
770
+ const value = (_a = reward.value) === null || _a === void 0 ? void 0 : _a.toString();
771
+ switch (reward.type) {
772
+ case "percentage":
773
+ case "cashback":
774
+ return value ? `${value}%` : "";
775
+ case "points":
776
+ return value ? `${value} points` : "";
777
+ case "fixed":
778
+ return value ? `$${value}` : "";
779
+ default:
780
+ return "";
781
+ }
782
+ }
783
+ }
784
+
785
+ /**
786
+ * HTTP client for communicating with CoBuy API
787
+ *
788
+ * NOTE: Backend API endpoints are currently under development.
789
+ * All API calls are designed to fail gracefully with proper error handling,
790
+ * ensuring the SDK continues to function (with placeholder UI) even when
791
+ * backend endpoints are not yet available.
792
+ */
793
+ class ApiClient {
794
+ constructor(config) {
795
+ this.baseUrl = config.baseUrl;
796
+ this.merchantKey = config.merchantKey;
797
+ this.logger = new Logger(config.debug || false);
798
+ this.defaultTimeout = config.timeout || 30000; // 30 seconds default
799
+ }
800
+ /**
801
+ * Make an HTTP request to the API
802
+ */
803
+ async request(endpoint, options = {}) {
804
+ const url = `${this.baseUrl}${endpoint}`;
805
+ const method = options.method || "GET";
806
+ const timeout = options.timeout || this.defaultTimeout;
807
+ this.logger.info(`API Request: ${method} ${url}`);
808
+ try {
809
+ const controller = new AbortController();
810
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
811
+ const response = await fetch(url, {
812
+ method,
813
+ headers: Object.assign({ "Content-Type": "application/json", "X-CoBuy-Merchant-Key": this.merchantKey, "X-CoBuy-SDK-Version": "1.0.0" }, options.headers),
814
+ body: options.body ? JSON.stringify(options.body) : undefined,
815
+ signal: controller.signal,
816
+ });
817
+ clearTimeout(timeoutId);
818
+ const responseData = await response.json();
819
+ if (!response.ok) {
820
+ this.logger.error(`API Error: ${response.status} ${response.statusText}`, responseData);
821
+ return {
822
+ success: false,
823
+ error: {
824
+ message: responseData.message || response.statusText,
825
+ code: responseData.code,
826
+ statusCode: response.status,
827
+ },
828
+ };
829
+ }
830
+ this.logger.info(`API Response: ${method} ${url} - Success`);
831
+ return {
832
+ success: true,
833
+ data: responseData,
834
+ };
835
+ }
836
+ catch (error) {
837
+ if (error instanceof Error) {
838
+ if (error.name === "AbortError") {
839
+ this.logger.error(`API Timeout: ${method} ${url}`);
840
+ return {
841
+ success: false,
842
+ error: {
843
+ message: "Request timeout",
844
+ code: "TIMEOUT",
845
+ },
846
+ };
847
+ }
848
+ this.logger.error(`API Error: ${method} ${url}`, error);
849
+ return {
850
+ success: false,
851
+ error: {
852
+ message: error.message,
853
+ code: "NETWORK_ERROR",
854
+ },
855
+ };
856
+ }
857
+ return {
858
+ success: false,
859
+ error: {
860
+ message: "Unknown error occurred",
861
+ code: "UNKNOWN_ERROR",
862
+ },
863
+ };
864
+ }
865
+ }
866
+ /**
867
+ * Make a GET request
868
+ */
869
+ async get(endpoint, options) {
870
+ return this.request(endpoint, Object.assign(Object.assign({}, options), { method: "GET" }));
871
+ }
872
+ /**
873
+ * Make a POST request
874
+ */
875
+ async post(endpoint, body, options) {
876
+ return this.request(endpoint, Object.assign(Object.assign({}, options), { method: "POST", body }));
877
+ }
878
+ /**
879
+ * Make a PUT request
880
+ */
881
+ async put(endpoint, body, options) {
882
+ return this.request(endpoint, Object.assign(Object.assign({}, options), { method: "PUT", body }));
883
+ }
884
+ /**
885
+ * Make a PATCH request
886
+ */
887
+ async patch(endpoint, body, options) {
888
+ return this.request(endpoint, Object.assign(Object.assign({}, options), { method: "PATCH", body }));
889
+ }
890
+ /**
891
+ * Make a DELETE request
892
+ */
893
+ async delete(endpoint, options) {
894
+ return this.request(endpoint, Object.assign(Object.assign({}, options), { method: "DELETE" }));
895
+ }
896
+ /**
897
+ * Update the base URL
898
+ */
899
+ setBaseUrl(baseUrl) {
900
+ this.baseUrl = baseUrl;
901
+ this.logger.info(`API Base URL updated: ${baseUrl}`);
902
+ }
903
+ /**
904
+ * Update the merchant key
905
+ */
906
+ setMerchantKey(merchantKey) {
907
+ this.merchantKey = merchantKey;
908
+ this.logger.info("Merchant key updated");
909
+ }
910
+ /**
911
+ * Get product reward information
912
+ *
913
+ * Retrieves reward data and eligibility for a specific product.
914
+ * This is a core method for SCRUM-247: Retrieve reward information.
915
+ *
916
+ * @param productId - The product ID to fetch reward information for
917
+ * @returns Promise with reward data including eligibility status
918
+ */
919
+ async getProductReward(productId) {
920
+ var _a;
921
+ const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_REWARD, { productId });
922
+ this.logger.info(`Fetching reward for product: ${productId}`);
923
+ const response = await this.get(endpoint);
924
+ // Handle the nested response structure from backend
925
+ if (response.success && ((_a = response.data) === null || _a === void 0 ? void 0 : _a.data)) {
926
+ return {
927
+ success: true,
928
+ data: response.data.data,
929
+ };
930
+ }
931
+ // Return error response
932
+ return {
933
+ success: false,
934
+ error: response.error || {
935
+ message: "Failed to fetch reward data",
936
+ code: "REWARD_FETCH_ERROR",
937
+ },
938
+ };
939
+ }
940
+ }
941
+
942
+ /**
943
+ * AnalyticsClient handles SDK event tracking and analytics
944
+ * Uses session ID provided by the SDK for all analytics events
945
+ */
946
+ class AnalyticsClient {
947
+ constructor(_merchantKey, sdkVersion, sessionId, apiClient, debug = false) {
948
+ this.logger = new Logger(debug);
949
+ this.apiClient = apiClient;
950
+ this.sdkVersion = sdkVersion;
951
+ this.sessionId = sessionId;
952
+ this.logger.debug(`[Analytics] Initialized with session ID: ${sessionId}`);
953
+ }
954
+ /**
955
+ * Track a CTA click event
956
+ */
957
+ async trackCtaClick(productId) {
958
+ const event = {
959
+ event: "cta_clicked",
960
+ productId,
961
+ timestamp: new Date().toISOString(),
962
+ sessionId: this.sessionId,
963
+ context: {
964
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
965
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
966
+ sdkVersion: this.sdkVersion,
967
+ },
968
+ };
969
+ try {
970
+ await this.sendEvent(event);
971
+ this.logger.info(`[Analytics] CTA click tracked for product: ${productId}`);
972
+ }
973
+ catch (error) {
974
+ // Log but don't throw - analytics failure should not break UX
975
+ this.logger.error("[Analytics] Failed to track CTA click", error);
976
+ }
977
+ }
978
+ /**
979
+ * Send event to backend analytics endpoint using unified ApiClient
980
+ */
981
+ async sendEvent(event) {
982
+ var _a, _b;
983
+ if (!this.apiClient) {
984
+ this.logger.warn("[Analytics] ApiClient not available, skipping event");
985
+ return;
986
+ }
987
+ try {
988
+ const response = await this.apiClient.request("/v1/sdk/analytics", {
989
+ method: "POST",
990
+ body: event,
991
+ });
992
+ if (!response.success) {
993
+ throw new Error(((_a = response.error) === null || _a === void 0 ? void 0 : _a.message) || "Failed to send analytics event");
994
+ }
995
+ this.logger.debug("[Analytics] Event recorded", { id: (_b = response.data) === null || _b === void 0 ? void 0 : _b.id });
996
+ }
997
+ catch (error) {
998
+ this.logger.error("[Analytics] Failed to send event", error);
999
+ // Re-throw for caller to handle if needed
1000
+ throw error;
1001
+ }
1002
+ }
1003
+ /**
1004
+ * Track custom event (extensible for future use)
1005
+ */
1006
+ async trackEvent(eventName, productId, metadata) {
1007
+ const event = {
1008
+ event: eventName,
1009
+ productId,
1010
+ timestamp: new Date().toISOString(),
1011
+ sessionId: this.sessionId,
1012
+ context: Object.assign({ pageUrl: typeof window !== "undefined" ? window.location.href : undefined, userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined, sdkVersion: this.sdkVersion }, metadata),
1013
+ };
1014
+ try {
1015
+ await this.sendEvent(event);
1016
+ this.logger.info(`[Analytics] Event tracked: ${eventName}`);
1017
+ }
1018
+ catch (error) {
1019
+ this.logger.error(`[Analytics] Failed to track event: ${eventName}`, error);
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ /**
1025
+ * SDK version constant
1026
+ */
1027
+ const SDK_VERSION = "1.0.0";
1028
+ /**
1029
+ * Main CoBuy SDK implementation
1030
+ */
1031
+ class CoBuy {
1032
+ constructor() {
1033
+ this.apiClient = null;
1034
+ this.analyticsClient = null;
1035
+ this.sessionId = "";
1036
+ this.SESSION_STORAGE_KEY = "cobuy_sdk_session_id";
1037
+ this.configManager = new ConfigManager();
1038
+ this.logger = new Logger(false);
1039
+ }
1040
+ /**
1041
+ * Initialize or retrieve existing session ID from storage
1042
+ */
1043
+ initializeSessionId() {
1044
+ // Try to retrieve from localStorage
1045
+ if (typeof window !== "undefined" && window.localStorage) {
1046
+ const stored = window.localStorage.getItem(this.SESSION_STORAGE_KEY);
1047
+ if (stored) {
1048
+ this.logger.debug(`[SDK] Using existing session ID: ${stored}`);
1049
+ return stored;
1050
+ }
1051
+ }
1052
+ // Generate new UUID v4 session ID
1053
+ const newSessionId = this.generateUUID();
1054
+ // Store in localStorage for persistence across page reloads
1055
+ if (typeof window !== "undefined" && window.localStorage) {
1056
+ try {
1057
+ window.localStorage.setItem(this.SESSION_STORAGE_KEY, newSessionId);
1058
+ }
1059
+ catch (e) {
1060
+ // Silently fail if localStorage unavailable (private mode, etc.)
1061
+ this.logger.warn("[SDK] Could not persist session ID to localStorage", e);
1062
+ }
1063
+ }
1064
+ this.logger.debug(`[SDK] Generated new session ID: ${newSessionId}`);
1065
+ return newSessionId;
1066
+ }
1067
+ /**
1068
+ * Generate UUID v4 compatible ID
1069
+ */
1070
+ generateUUID() {
1071
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
1072
+ return crypto.randomUUID();
1073
+ }
1074
+ // Fallback for environments without crypto.randomUUID
1075
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1076
+ }
1077
+ /**
1078
+ * Get the current session ID (core SDK concept)
1079
+ */
1080
+ getSessionId() {
1081
+ return this.sessionId;
1082
+ }
1083
+ /**
1084
+ * Initialize the SDK with configuration
1085
+ */
1086
+ init(options) {
1087
+ try {
1088
+ const config = this.configManager.setConfig(options);
1089
+ this.logger.setDebug(config.debug);
1090
+ this.logger.info(`Initialized with environment: ${config.env}`);
1091
+ this.logger.info(`API Base URL: ${config.apiBaseUrl}`);
1092
+ // Initialize session ID (core SDK concept, used by all services)
1093
+ this.sessionId = this.initializeSessionId();
1094
+ // Initialize API client
1095
+ this.apiClient = new ApiClient({
1096
+ baseUrl: config.apiBaseUrl,
1097
+ merchantKey: config.merchantKey,
1098
+ debug: config.debug,
1099
+ });
1100
+ // Initialize Analytics client with the SDK's session ID
1101
+ this.analyticsClient = new AnalyticsClient(config.merchantKey, SDK_VERSION, this.sessionId, this.apiClient, config.debug);
1102
+ }
1103
+ catch (error) {
1104
+ this.logger.error("Failed to initialize SDK", error);
1105
+ throw error;
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Render the CoBuy widget into a DOM container
1110
+ */
1111
+ renderWidget(options) {
1112
+ // Check if SDK is initialized
1113
+ if (!this.configManager.isInitialized()) {
1114
+ throw new CoBuyNotInitializedError();
1115
+ }
1116
+ // Validate productId
1117
+ if (!options.productId) {
1118
+ const errorMessage = "productId is required for renderWidget";
1119
+ this.logger.error(errorMessage);
1120
+ if (options.onError) {
1121
+ options.onError(new Error(errorMessage));
1122
+ }
1123
+ return;
1124
+ }
1125
+ try {
1126
+ const config = this.configManager.getConfig();
1127
+ const widget = new WidgetRoot(config, this.apiClient, this.analyticsClient);
1128
+ widget.render(options);
1129
+ }
1130
+ catch (error) {
1131
+ this.logger.error("Failed to render widget", error);
1132
+ if (options.onError) {
1133
+ options.onError(error);
1134
+ }
1135
+ }
1136
+ }
1137
+ /**
1138
+ * Open modal for product details
1139
+ */
1140
+ openModal(options) {
1141
+ var _a;
1142
+ if (!this.configManager.isInitialized()) {
1143
+ this.logger.warn("SDK not initialized, cannot open modal");
1144
+ return;
1145
+ }
1146
+ this.logger.info(`Opening modal for product: ${options.productId}`);
1147
+ try {
1148
+ // Find or create modal container
1149
+ const modalContainer = document.getElementById("cobuy-modal-root");
1150
+ if (!modalContainer) {
1151
+ this.logger.warn("Modal container #cobuy-modal-root not found. Create an element with id='cobuy-modal-root' to use modals.");
1152
+ return;
1153
+ }
1154
+ // Show modal (placeholder - will be replaced with actual modal implementation)
1155
+ modalContainer.style.display = "block";
1156
+ modalContainer.setAttribute("data-product-id", options.productId);
1157
+ // Call callback if provided
1158
+ if (options.onOpen) {
1159
+ options.onOpen(options.productId);
1160
+ }
1161
+ // Emit event if configured
1162
+ const config = this.configManager.getConfig();
1163
+ if ((_a = config.events) === null || _a === void 0 ? void 0 : _a.onModalOpen) {
1164
+ config.events.onModalOpen(options.productId);
1165
+ }
1166
+ }
1167
+ catch (error) {
1168
+ this.logger.error("Failed to open modal", error);
1169
+ }
1170
+ }
1171
+ /**
1172
+ * Close the modal
1173
+ */
1174
+ closeModal() {
1175
+ var _a;
1176
+ try {
1177
+ const modalContainer = document.getElementById("cobuy-modal-root");
1178
+ if (modalContainer) {
1179
+ modalContainer.style.display = "none";
1180
+ const currentProductId = modalContainer.getAttribute("data-product-id");
1181
+ // Emit event if configured
1182
+ const config = this.configManager.getConfig();
1183
+ if (((_a = config.events) === null || _a === void 0 ? void 0 : _a.onModalClose) && currentProductId) {
1184
+ config.events.onModalClose(currentProductId);
1185
+ }
1186
+ }
1187
+ this.logger.info("Modal closed");
1188
+ }
1189
+ catch (error) {
1190
+ this.logger.error("Failed to close modal", error);
1191
+ }
1192
+ }
1193
+ /**
1194
+ * Get the initialized API client instance
1195
+ */
1196
+ getApiClient() {
1197
+ return this.apiClient;
1198
+ }
1199
+ /**
1200
+ * Get the initialized Analytics client instance
1201
+ */
1202
+ getAnalyticsClient() {
1203
+ return this.analyticsClient;
1204
+ }
1205
+ /**
1206
+ * Get SDK version
1207
+ */
1208
+ get version() {
1209
+ return SDK_VERSION;
1210
+ }
1211
+ }
1212
+
1213
+ // Create singleton instance
1214
+ const instance = new CoBuy();
1215
+ // Attach to window if in browser environment
1216
+ if (typeof window !== "undefined") {
1217
+ if (window.CoBuy) {
1218
+ console.warn("[CoBuy] Multiple SDK bundles detected. This may cause unexpected behavior.");
1219
+ }
1220
+ else {
1221
+ window.CoBuy = instance;
1222
+ }
1223
+ }
1224
+
1225
+ export { instance as default };
1226
+ //# sourceMappingURL=cobuy-sdk.esm.js.map