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