@cshah18/sdk 3.0.5 → 4.1.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.
@@ -304,11 +304,44 @@ class GroupListModal {
304
304
  this.currentSessionId = null;
305
305
  this.onGroupJoined = null;
306
306
  this.onViewProgress = null;
307
+ this.socketListenerRegistered = false;
307
308
  this.escapeHandler = (event) => {
308
309
  if (event.key === "Escape") {
309
310
  this.close();
310
311
  }
311
312
  };
313
+ this.handleGroupMemberJoinedEvent = (event) => {
314
+ this.onGroupMemberJoined(event);
315
+ };
316
+ /** Handle group member joined socket event and update groups list */
317
+ this.onGroupMemberJoined = (event) => {
318
+ const detail = event.detail || {};
319
+ const productId = detail.product_id;
320
+ const groupData = detail.group;
321
+ // Only process if this is for the current product
322
+ if (!productId || !this.currentProductId || productId !== this.currentProductId) {
323
+ return;
324
+ }
325
+ if (!groupData) {
326
+ return;
327
+ }
328
+ const groupId = groupData.id;
329
+ const participantsCount = groupData.participants_count;
330
+ if (!groupId) {
331
+ return;
332
+ }
333
+ // Find and update the group in the list
334
+ const groupIndex = this.groups.findIndex((g) => g.groupId === groupId);
335
+ if (groupIndex !== -1) {
336
+ // Update the joined count
337
+ if (typeof participantsCount === "number") {
338
+ this.groups[groupIndex].joined = participantsCount;
339
+ }
340
+ this.logger.info(`[GroupListModal] Updated group ${groupId} - participants: ${participantsCount}`);
341
+ // Re-render the specific group card
342
+ this.updateGroupCard(groupId);
343
+ }
344
+ };
312
345
  this.logger = new Logger(debug);
313
346
  this.groups = groups;
314
347
  this.liveCount = liveCount;
@@ -335,6 +368,65 @@ class GroupListModal {
335
368
  hasGroup(groupId) {
336
369
  return this.groups.some((group) => group.groupId === groupId);
337
370
  }
371
+ /** Subscribe to socket events for real-time group updates */
372
+ subscribeToSocketEvents() {
373
+ if (typeof window === "undefined" || this.socketListenerRegistered) {
374
+ return;
375
+ }
376
+ window.addEventListener("group:member:joined", this.handleGroupMemberJoinedEvent);
377
+ this.socketListenerRegistered = true;
378
+ this.logger.debug("[GroupListModal] Socket event listeners registered");
379
+ }
380
+ /** Unsubscribe from socket events */
381
+ unsubscribeFromSocketEvents() {
382
+ if (typeof window === "undefined" || !this.socketListenerRegistered) {
383
+ return;
384
+ }
385
+ window.removeEventListener("group:member:joined", this.handleGroupMemberJoinedEvent);
386
+ this.socketListenerRegistered = false;
387
+ this.logger.debug("[GroupListModal] Socket event listeners unregistered");
388
+ }
389
+ /** Update the rendered group card with new data */
390
+ updateGroupCard(groupId) {
391
+ if (!this.overlayEl) {
392
+ return;
393
+ }
394
+ const groupCard = this.overlayEl.querySelector(`[data-group-id="${groupId}"]`);
395
+ if (!groupCard) {
396
+ return;
397
+ }
398
+ const group = this.groups.find((g) => g.groupId === groupId);
399
+ if (!group) {
400
+ return;
401
+ }
402
+ // Update the progress info
403
+ const remaining = Math.max(group.total - group.joined, 0);
404
+ const joinedCountEl = groupCard.querySelector(".cobuy-joined-count");
405
+ const spotsLeftEl = groupCard.querySelector(".cobuy-spots-left");
406
+ if (joinedCountEl) {
407
+ joinedCountEl.textContent = `${group.joined} Joined`;
408
+ }
409
+ if (spotsLeftEl) {
410
+ spotsLeftEl.textContent = `${remaining} ${remaining === 1 ? "spot" : "spots"} left`;
411
+ }
412
+ // Update the progress bar
413
+ const progressFill = groupCard.querySelector(".cobuy-gl-progress-fill");
414
+ if (progressFill) {
415
+ const pct = Math.max(0, Math.min(100, Math.round((group.joined / group.total) * 100)));
416
+ progressFill.style.width = `${pct}%`;
417
+ }
418
+ // Update member avatars
419
+ const membersContainer = groupCard.querySelector(".cobuy-group-members");
420
+ if (membersContainer) {
421
+ // Clear existing avatars
422
+ membersContainer.innerHTML = "";
423
+ // Recreate avatars with updated joined count
424
+ for (let i = 0; i < group.total; i++) {
425
+ const avatar = this.createMemberAvatar(i >= group.joined);
426
+ membersContainer.appendChild(avatar);
427
+ }
428
+ }
429
+ }
338
430
  async open(productId, sessionId, joinedGroupId) {
339
431
  if (this.overlayEl) {
340
432
  this.logger.debug("Group list modal already open");
@@ -380,6 +472,8 @@ class GroupListModal {
380
472
  this.overlayEl = overlay;
381
473
  document.body.appendChild(overlay);
382
474
  document.addEventListener("keydown", this.escapeHandler);
475
+ // Subscribe to socket events for real-time group updates
476
+ this.subscribeToSocketEvents();
383
477
  requestAnimationFrame(() => {
384
478
  overlay.classList.add("cobuy-gl-open");
385
479
  });
@@ -410,12 +504,14 @@ class GroupListModal {
410
504
  // Convert API groups to display format
411
505
  const groups = response.data.groups.map((apiGroup) => ({
412
506
  groupId: apiGroup.id,
507
+ name: apiGroup.group_name,
413
508
  timeLabel: this.formatTimeRemaining(apiGroup.timeLeftSeconds),
414
509
  timeLeftSeconds: apiGroup.timeLeftSeconds,
415
510
  joined: apiGroup.participants_count,
416
511
  total: apiGroup.max_participants,
417
512
  isMember: Boolean(apiGroup.is_member),
418
513
  }));
514
+ console.log("groupsss", groups);
419
515
  // If API reports membership, prefer it over cached state
420
516
  const memberGroup = groups.find((g) => g.isMember);
421
517
  if (memberGroup) {
@@ -477,6 +573,8 @@ class GroupListModal {
477
573
  if (!this.overlayEl) {
478
574
  return;
479
575
  }
576
+ // Unsubscribe from socket events
577
+ this.unsubscribeFromSocketEvents();
480
578
  // Clear any running countdown intervals to avoid leaks
481
579
  this.countdownIntervals.forEach((id) => window.clearInterval(id));
482
580
  this.countdownIntervals = [];
@@ -720,11 +818,12 @@ class GroupListModal {
720
818
  createGroupCard(group) {
721
819
  const card = document.createElement("div");
722
820
  card.className = "cobuy-group-card";
821
+ card.setAttribute("data-group-id", group.name || group.groupId);
723
822
  const header = document.createElement("div");
724
823
  header.className = "cobuy-group-card-header";
725
824
  const groupId = document.createElement("div");
726
825
  groupId.className = "cobuy-group-id";
727
- groupId.textContent = `Group #${group.groupId}`;
826
+ groupId.textContent = `Group #${group.name || group.groupId}`;
728
827
  const timer = document.createElement("div");
729
828
  timer.className = "cobuy-group-timer";
730
829
  timer.innerHTML = `
@@ -1263,6 +1362,158 @@ class GroupListModal {
1263
1362
  }
1264
1363
  }
1265
1364
 
1365
+ /**
1366
+ * Offline Redemption Utilities
1367
+ * Helpers for constructing QR URLs, copying codes, and downloading QR images
1368
+ */
1369
+ const S3_BUCKET = "cobuy-dev";
1370
+ const S3_REGION = "af-south-1";
1371
+ /**
1372
+ * Build full S3 URL for QR code image
1373
+ * @param qrCodeData - S3 key from offline_redemption.qr_code_data
1374
+ * @returns Full HTTPS URL to the QR code image
1375
+ */
1376
+ function buildQRUrl(qrCodeData) {
1377
+ if (!qrCodeData) {
1378
+ return "";
1379
+ }
1380
+ const prefix = `https://${S3_BUCKET}.s3.${S3_REGION}.amazonaws.com/`;
1381
+ return `${prefix}${qrCodeData}`;
1382
+ }
1383
+ /**
1384
+ * Copy redemption code to clipboard
1385
+ * @param code - Redemption code to copy
1386
+ * @returns Promise that resolves when copy is complete
1387
+ */
1388
+ async function copyRedemptionCode(code) {
1389
+ const logger = new Logger(false);
1390
+ if (!code) {
1391
+ logger.warn("Cannot copy empty redemption code");
1392
+ return false;
1393
+ }
1394
+ try {
1395
+ // Use modern clipboard API if available
1396
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1397
+ await navigator.clipboard.writeText(code);
1398
+ logger.info("Redemption code copied to clipboard");
1399
+ return true;
1400
+ }
1401
+ // Fallback for older browsers
1402
+ const textarea = document.createElement("textarea");
1403
+ textarea.value = code;
1404
+ textarea.style.position = "fixed";
1405
+ textarea.style.opacity = "0";
1406
+ document.body.appendChild(textarea);
1407
+ textarea.select();
1408
+ textarea.setSelectionRange(0, 99999); // For mobile devices
1409
+ const successful = document.execCommand("copy");
1410
+ document.body.removeChild(textarea);
1411
+ if (successful) {
1412
+ logger.info("Redemption code copied to clipboard (fallback method)");
1413
+ return true;
1414
+ }
1415
+ return false;
1416
+ }
1417
+ catch (error) {
1418
+ logger.error("Failed to copy redemption code", error);
1419
+ return false;
1420
+ }
1421
+ }
1422
+ /**
1423
+ * Download QR code image
1424
+ * @param qrUrl - Full URL to the QR code image
1425
+ * @param filename - Name for the downloaded file
1426
+ */
1427
+ async function downloadQRCode(qrUrl, filename = "redemption-qr.png") {
1428
+ const logger = new Logger(false);
1429
+ if (!qrUrl) {
1430
+ logger.warn("Cannot download QR code - empty URL");
1431
+ return false;
1432
+ }
1433
+ try {
1434
+ const response = await fetch(qrUrl);
1435
+ if (!response.ok) {
1436
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1437
+ }
1438
+ const blob = await response.blob();
1439
+ const blobUrl = window.URL.createObjectURL(blob);
1440
+ const link = document.createElement("a");
1441
+ link.href = blobUrl;
1442
+ link.download = filename;
1443
+ link.style.display = "none";
1444
+ document.body.appendChild(link);
1445
+ link.click();
1446
+ document.body.removeChild(link);
1447
+ window.URL.revokeObjectURL(blobUrl);
1448
+ logger.info("QR code downloaded successfully");
1449
+ return true;
1450
+ }
1451
+ catch (error) {
1452
+ logger.error("Failed to download QR code", error);
1453
+ return false;
1454
+ }
1455
+ }
1456
+ /**
1457
+ * Validate offline redemption data
1458
+ * @param redemption - Offline redemption object
1459
+ * @returns true if data is valid and complete
1460
+ */
1461
+ function isValidOfflineRedemption(redemption) {
1462
+ if (!redemption || typeof redemption !== "object") {
1463
+ return false;
1464
+ }
1465
+ const obj = redemption;
1466
+ return (typeof obj.member_id === "string" &&
1467
+ obj.member_id.length > 0 &&
1468
+ typeof obj.qr_code_value === "string" &&
1469
+ obj.qr_code_value.length > 0 &&
1470
+ typeof obj.redemption_code === "string" &&
1471
+ obj.redemption_code.length > 0 &&
1472
+ typeof obj.qr_code_data === "string" &&
1473
+ obj.qr_code_data.length > 0 &&
1474
+ typeof obj.offline_expires_at === "string" &&
1475
+ obj.offline_expires_at.length > 0);
1476
+ }
1477
+ /**
1478
+ * Format expiry date for display
1479
+ * @param isoDate - ISO 8601 date string
1480
+ * @returns Formatted date string (e.g., "Feb 20, 2026 at 10:21 AM")
1481
+ */
1482
+ function formatExpiryDate(isoDate) {
1483
+ try {
1484
+ const date = new Date(isoDate);
1485
+ // Check if date is valid
1486
+ if (isNaN(date.getTime())) {
1487
+ return "Invalid date";
1488
+ }
1489
+ return date.toLocaleDateString("en-US", {
1490
+ year: "numeric",
1491
+ month: "short",
1492
+ day: "numeric",
1493
+ hour: "2-digit",
1494
+ minute: "2-digit",
1495
+ });
1496
+ }
1497
+ catch (_a) {
1498
+ return "Invalid date";
1499
+ }
1500
+ }
1501
+ /**
1502
+ * Check if offline redemption is still valid
1503
+ * @param expiryDate - ISO 8601 expiry date string
1504
+ * @returns true if redemption has not expired
1505
+ */
1506
+ function isRedemptionValid(expiryDate) {
1507
+ try {
1508
+ const expiry = new Date(expiryDate);
1509
+ const now = new Date();
1510
+ return now < expiry;
1511
+ }
1512
+ catch (_a) {
1513
+ return false;
1514
+ }
1515
+ }
1516
+
1266
1517
  function styleInject(css, ref) {
1267
1518
  if ( ref === void 0 ) ref = {};
1268
1519
  var insertAt = ref.insertAt;
@@ -1290,7 +1541,7 @@ function styleInject(css, ref) {
1290
1541
  }
1291
1542
  }
1292
1543
 
1293
- var css_248z = ".cb-lobby-modal-container{height:100%;left:0;position:fixed;top:0;width:100%;z-index:10000}.cb-lobby-modal-container *{box-sizing:border-box;margin:0;padding:0}.cb-lobby-modal-container body,.cb-lobby-modal-container html{font-family:Inter,sans-serif;height:100%;overflow-x:hidden;width:100%}.cb-lobby-main{backdrop-filter:blur(4px);background-color:color-mix(in oklab,#fff 5%,transparent);border-radius:50px;box-shadow:0 25px 50px -12px #00000040;padding:80px;position:relative;width:100%;z-index:1}.cb-lobby-bg{background-image:url(https://cobuy-dev.s3.af-south-1.amazonaws.com/public/sdk/cb-back-image.png);background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:-1}.cb-lobby-main-wp{align-items:center;display:flex;justify-content:center;padding:40px 80px}.lobby-status{background:#fff;border-radius:4px;color:#000;font-size:10px;font-weight:600;line-height:1.2;padding:6px 10px;text-transform:uppercase}.lobby-status.active{background:#155dfc;color:#fff}.lobby-status.complete{background:green;color:#fff}.lobby-status-wp{align-items:center;display:flex;flex-wrap:wrap;gap:10px}.lobby-number{backdrop-filter:blur(3px);background:hsla(0,0%,100%,.2);border-radius:4px;box-shadow:0 25px 50px -12px #00000040;font-size:12px;font-weight:700;letter-spacing:1px;line-height:1.2;padding:5px 8px;text-transform:uppercase}.title-wp{margin-top:25px}.title-wp h2{font-size:60px;font-weight:900;line-height:1;margin-bottom:15px}.sub-title{font-size:20px;line-height:1;margin-bottom:10px}.lobby-link-box{align-items:center;backdrop-filter:blur(12px);background-color:hsla(0,0%,100%,.4);border:1px solid hsla(0,0%,100%,.2);border-radius:1rem;box-shadow:0 1px 3px 0 #0000001a;display:flex;flex:1;gap:20px;justify-content:space-between;padding:13px 20px}.lobby-link-text{font-size:12px;font-weight:600;line-height:1;margin-bottom:6px;text-transform:uppercase}.copy-link-btn{align-items:center;color:#1d293d;cursor:pointer;display:flex;justify-content:center;padding:5px}.lobby-link-url{font-size:16px;font-weight:700;line-height:1}.lobby-link-wp{align-items:center;display:flex;gap:20px;justify-content:space-between;margin-top:40px}.share-btn-wp .share-btn{align-items:center;background:#000;border-radius:15px;color:#fff;cursor:pointer;display:flex;height:68px;justify-content:center;padding:5px;width:68px}.lobby-offer-box{align-items:center;backdrop-filter:blur(8px);background-color:rgba(59,130,246,.063);border:1px solid rgba(59,130,246,.125);border-radius:1rem;display:flex;gap:30px;margin-top:30px;padding:15px 20px}.offer-box-icon{align-items:center;background:linear-gradient(135deg,#3b82f6,rgba(59,130,246,.867));border-radius:10px;box-shadow:0 10px 15px -3px rgba(59,130,246,.314);color:#fff;cursor:pointer;display:flex;height:56px;justify-content:center;padding:5px;width:56px}.offer-lock-status{align-items:center;display:flex;gap:6px;margin-bottom:2px}.offer-lock-status span{font-size:14px;font-weight:700;line-height:1}.offer-box-content h3{font-size:30px;font-weight:900;line-height:1.2}.cb-lobby-top{display:grid;gap:100px;grid-template-columns:7fr 5fr}.group-info{backdrop-filter:blur(8px);background-color:color-mix(in oklab,#fff 5%,transparent);border-radius:20px;box-shadow:0 10px 15px -3px #0000001a;padding:30px}.progress-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:14px}.progress-header .title{align-items:center;color:#000;display:flex;font-size:14px;font-weight:900;justify-content:center;letter-spacing:.7px;position:relative}.progress-badge{background:#3b82f6;border-radius:999px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;color:#fff;font-size:13px;font-weight:700;padding:6px 12px}.cb-lobby-modal-container .progress-bar{background:#fff;border-radius:999px;height:14px;overflow:hidden;width:100%}.cb-lobby-modal-container .progress-fill{animation:shimmer 2.7s linear infinite;background:#3b82f6;background-image:linear-gradient(120deg,hsla(0,0%,100%,.15) 25%,hsla(0,0%,100%,.45) 37%,hsla(0,0%,100%,.15) 63%);background-size:200% 100%;border-radius:999px;height:100%;position:relative;transition:width .6s ease;width:0}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.progress-labels{color:#474d56;display:flex;font-size:12px;justify-content:space-between;margin-top:8px}.team-card{margin-top:40px}.team-card .icon-box svg{width:16px}.team-card .team-header{align-items:center;display:flex;justify-content:space-between}.team-card .team-title{align-items:center;display:flex;font-size:14px;font-weight:600;gap:12px;letter-spacing:1px}.team-card .icon-box{align-items:center;background-color:#3b82f6;border-radius:.5rem;box-shadow:0 10px 15px -3px #3b82f64d;color:#fff;display:flex;font-size:18px;height:2rem;justify-content:center;width:2rem}.team-card .team-title span{color:#000;font-size:14px;font-weight:900;letter-spacing:.7px}.team-card .team-count{background-color:#3b82f6;border-radius:9999px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;color:#fff;font-size:13px;font-weight:700;letter-spacing:.7px;padding:6px 12px}.team-card .team-members{align-items:center;display:flex;gap:14px;justify-content:center;margin-top:20px}.team-card .member{border:3px solid #fff;border-radius:50%;box-shadow:0 4px 12px #0000001a;font-size:22px;height:56px;position:relative;width:56px}.team-card .member,.team-card .member:after{align-items:center;color:#fff;display:flex;justify-content:center}.team-card .member:after{background:#3b82f6;background-image:url(https://cobuy-dev.s3.af-south-1.amazonaws.com/public/sdk/tick-mark-icon.svg);background-position:50%;background-repeat:no-repeat;background-size:75%;border:2px solid #fff;border-radius:50%;bottom:-4px;content:\"\";height:12px;padding:2px;position:absolute;right:-4px;width:12px}.team-card .member.blue{background:#2563eb}.team-card .member.purple{background:#9333ea}.team-card .member.pink{background:#ec4899}.team-card .member.orange{background:#f97316}.team-card .member.empty{background:#f8fafc;color:#9ca3af}.team-card .member.empty:after{display:none}.time-card{align-items:center;backdrop-filter:blur(8px);background:hsla(0,0%,100%,.2);border:1px solid hsla(0,0%,100%,.2);border-radius:20px;display:flex;gap:14px;margin-top:30px;padding:15px}.time-card .time-icon{align-items:center;background:#3b82f6;border-radius:14px;color:#fff;display:flex;flex-shrink:0;font-size:22px;height:48px;justify-content:center;width:48px}.time-card .time-content{display:flex;flex-direction:column}.time-card .time-label{color:#4b5563;font-size:12px;font-weight:600;letter-spacing:.6px}.time-card .time-value{color:#111827;font-size:22px;font-weight:900}.group-info-box{backdrop-filter:blur(8px);background:color-mix(in oklab,#fff 5%,transparent);border:1px solid color-mix(in oklab,#fff 20%,transparent);border-radius:1.5rem;box-shadow:0 10px 15px -3px #0000001a;padding:30px}.offer-lock-status svg{height:16px;width:16px}.cb-lobby-modal-container .offer-box-content .reward-text{background:unset;border:none;color:#000;font-size:14px;line-height:1;margin-top:4px}.progress-header .title:before{background:#3b82f6;border-radius:50%;content:\"\";display:inline-block;height:8px;margin-right:8px;position:relative;width:8px}.live-activity-wrapper{backdrop-filter:blur(8px);background-color:color-mix(in oklab,#fff 80%,transparent);border-radius:18px;box-shadow:0 30px 80px rgba(0,0,0,.15);padding:16px}.live-activity-header{display:flex;gap:10px;justify-content:space-between;margin-bottom:12px}.live-activity-header .title{align-items:center;color:#000;display:flex;font-size:14px;font-weight:600;font-weight:900;gap:8px;letter-spacing:.7px}.live-activity-header .dot{background:#3b82f6;border-radius:50%;height:8px;position:relative;width:8px}.live-activity-header .dot:after{animation:livePulse 1.6s ease-out infinite;background:rgba(59,130,246,.6);border-radius:50%;content:\"\";inset:0;position:absolute}@keyframes livePulse{0%{opacity:.8;transform:scale(1)}70%{opacity:0;transform:scale(2.4)}to{opacity:0}}.activity-stats{align-items:center;display:flex;gap:8px}.activity-stats-badge{background-color:rgba(59,130,246,.082);border-radius:999px;color:#3b82f6;font-size:12px;font-weight:500;line-height:1;padding:4px 10px}.activity-stats-badge.light{background-color:#f9f3f4;color:#45556c}.activity-list{height:104px;overflow:hidden;position:relative}.activity-card{align-items:center;background:linear-gradient(90deg,#fff,#f2f6ff);border:1px solid #dbeafe;border-radius:14px;display:flex;gap:10px;height:58px;inset:0;padding:12px 10px;position:absolute;transition:transform .6s cubic-bezier(.22,.61,.36,1),opacity .6s}.activity-card .text{font-size:14px}.activity-card .avatar{align-items:center;border-radius:50%;color:#fff;display:flex;flex:0 0 30px;height:30px;justify-content:center;width:30px}.activity-card .pink{background:linear-gradient(135deg,#ec4899,#f472b6)}.activity-card .purple{background:linear-gradient(135deg,#8b5cf6,#a78bfa)}.activity-card .blue{background:linear-gradient(135deg,#3b82f6,#60a5fa)}.activity-card .green{background:linear-gradient(135deg,#10b981,#34d399)}.activity-card .orange{background:linear-gradient(135deg,#f97316,#fb923c)}.activity-card .text p{color:#6b7280;font-size:12px;margin:2px 0 0}.activity-card .time{color:#94a3b8;font-size:10px;margin-left:auto}.lobby-activity-wp{backdrop-filter:blur(12px);background-color:hsla(0,0%,100%,.4);border:1px solid hsla(0,0%,100%,.2);border-radius:25px;box-shadow:0 1px 3px 0 #0000001a;margin-top:50px;padding:8px}.lobby-close-icon{align-items:center;background-color:color-mix(in oklab,#000 80%,transparent);border-radius:50%;color:#fff;cursor:pointer;display:flex;height:34px;justify-content:center;position:fixed;right:30px;top:30px;width:34px;z-index:99}.lobby-close-icon svg{height:20px;width:20px}@media screen and (max-width:1600px){.cb-lobby-main{border-radius:30px;padding:50px}.title-wp h2{font-size:48px;margin-bottom:12px}.sub-title{font-size:16px}.title-wp{margin-top:20px}.lobby-link-wp{margin-top:30px}.offer-box-content h3{font-size:26px}.lobby-offer-box{gap:24px;padding:15px}.cb-lobby-top{gap:80px}.group-info-box{border-radius:20px;padding:22px}.team-card{margin-top:30px}.team-card .team-members{margin-top:12px}.lobby-activity-wp{margin-top:40px}.lobby-close-icon{height:30px;top:20px;width:30px}}@media screen and (max-width:1280px){.cb-lobby-main,.cb-lobby-main-wp{padding:40px}.title-wp h2{font-size:42px}.cb-lobby-top{gap:60px}}@media screen and (max-width:1120px){.title-wp h2{font-size:38px}}@media screen and (max-width:991px){.cb-lobby-top{gap:30px;grid-template-columns:1fr}.cb-lobby-main{border-radius:15px;padding:30px}.cb-lobby-main-wp{padding:30px}.lobby-close-icon{right:20px}.lobby-link-box{border-radius:10px}.offer-box-content h3{font-size:24px}.lobby-activity-wp,.lobby-offer-box,.time-card{border-radius:15px;margin-top:20px}.live-activity-wrapper{border-radius:10px}.group-info-box{border-radius:15px}}@media screen and (max-width:575px){.cb-lobby-main,.cb-lobby-main-wp{padding:20px}.title-wp h2{font-size:30px}.lobby-link-text{font-size:11px;margin-bottom:4px}.lobby-link-url{font-size:14px}.lobby-link-box{padding:10px}.lobby-link-wp{gap:12px}.share-btn-wp .share-btn{border-radius:8px;height:52px;width:52px}.lobby-offer-box{padding:10px}.offer-box-content h3{font-size:20px}.offer-box-content .reward-text{font-size:13px}.group-info-box{padding:13px}.progress-header .title,.team-card .team-title span{font-size:13px}.team-card .member{height:40px;width:40px}.team-card .member:after{height:8px;width:8px}.team-card .team-members{gap:10px;margin-top:12px}.time-card{padding:13px}.team-card{margin-top:22px}.activity-card .text{font-size:12px}.activity-card .text p{font-size:11px}.live-activity-header .title{font-size:12px}.time-card .time-value{font-size:20px}}@media screen and (max-width:480px){.cb-lobby-main-wp{padding:20px 12px}.cb-lobby-main{padding:15px 12px}.lobby-status{font-size:9px}.lobby-number{font-size:10px}.title-wp h2{font-size:28px;margin-bottom:7px}.sub-title{font-size:14px}.lobby-link-wp{gap:10px;margin-top:20px}.lobby-offer-box{gap:12px}.offer-box-icon{height:45px;width:45px}.offer-box-content .reward-text,.offer-lock-status span{font-size:12px}.cb-lobby-top{gap:20px}.lobby-close-icon{right:10px;top:10px}.live-activity-header{flex-direction:column;gap:5px}.activity-card{border-radius:9px;padding:6px}}.share-overlay{align-items:center;background:rgba(0,0,0,.45);display:flex;inset:0;justify-content:center;opacity:0;position:fixed;transition:.3s ease;visibility:hidden;z-index:999}.share-overlay.active{opacity:1;visibility:visible}.share-popup{background:#fff;border-radius:18px;max-height:90%;overflow:auto;padding:24px;position:relative;transform:translateY(30px);transition:.35s ease;width:420px}.share-overlay.active .share-popup{transform:translateY(0)}.share-popup .close-btn{align-items:center;background:#f3f4f6;border:none;border-radius:50%;cursor:pointer;display:flex;height:32px;justify-content:center;position:absolute;right:14px;top:14px;transition:.3s;width:32px}.share-popup .close-btn:hover{background:#eeecec}.share-popup h2{font-size:22px;font-weight:600;line-height:1;margin-bottom:10px}.share-popup .subtitle{color:#4a5565;font-size:14px;margin:6px 0 30px}.share-popup .share-grid{display:grid;gap:14px;grid-template-columns:repeat(2,1fr)}.share-popup .share-card{align-items:center;background:#f2f6f9;border-radius:14px;cursor:pointer;display:flex;flex-direction:column;padding:16px;position:relative;text-align:center;transition:.25s ease}.share-popup .share-card:hover{transform:translateY(-3px)}.share-popup .share-card .icon{align-items:center;background:#1877f2;border-radius:50%;color:#fff;display:flex;font-size:30px;height:50px;justify-content:center;margin-bottom:8px;text-align:center;width:50px}.share-popup .share-card span{font-size:13px;font-weight:500}.share-popup .share-card.whatsapp{background:#ecfdf5;border:2px solid #22c55e;color:#22c55e}.share-popup .share-card.whatsapp .icon{background:#22c55e}.share-popup .share-card .badge{background:#2563eb;border-radius:12px;color:#fff;font-size:11px;padding:4px 10px;position:absolute;right:10px;top:-8px}.share-popup .link-box{background:#fbf9fa;border:1px solid #ebe6e7;border-radius:14px;margin-top:18px;padding:14px}.share-popup .link-box label{color:#64748b;font-size:13px}.share-popup .link-row{display:flex;gap:8px;margin-top:6px}.share-popup .link-row input{background:transparent;border:none;color:#334155;flex:1;font-size:13px;letter-spacing:.8px;line-height:1.2;outline:none}.share-popup .link-row button{align-items:center;background:#fff;border:1px solid #ebe6e7;border-radius:10px;cursor:pointer;display:flex;height:35px;justify-content:center;padding:6px 8px;width:35px}.share-popup .success{color:#2563eb;display:block;font-size:12px;margin-top:6px;opacity:0;transition:opacity .3s}.share-popup .success.show{opacity:1}.share-popup .footer-text{color:#64748b;font-size:12px;margin-top:14px;text-align:center}.share-popup .share-card.twitter .icon{background:#000}.share-popup .share-card.facebook .icon{background:#1877f2}.share-popup .share-card.sms .icon{background:#059669}.share-popup .share-card.copied .icon{background:#2563eb}";
1544
+ var css_248z = ".cb-lobby-modal-container{height:100%;left:0;position:fixed;top:0;width:100%;z-index:10000}.cb-lobby-modal-container *{box-sizing:border-box;margin:0;padding:0}.cb-lobby-modal-container body,.cb-lobby-modal-container html{font-family:Inter,sans-serif;height:100%;overflow-x:hidden;width:100%}.cb-lobby-main{backdrop-filter:blur(4px);background-color:color-mix(in oklab,#fff 5%,transparent);border-radius:50px;box-shadow:0 25px 50px -12px #00000040;padding:80px;position:relative;width:100%;z-index:1}.cb-lobby-bg{background-image:url(https://cobuy-dev.s3.af-south-1.amazonaws.com/public/sdk/cb-back-image.png);background-position:50%;background-repeat:no-repeat;background-size:cover;height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:-1}.cb-lobby-main-wp{align-items:center;display:flex;justify-content:center;padding:40px 80px}.lobby-indicator{backdrop-filter:blur(8px);background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);border-radius:24px;gap:8px;margin-bottom:16px;padding:8px 16px;width:fit-content}.lobby-indicator,.pulsing-dot{align-items:center;display:flex}.pulsing-dot{height:8px;justify-content:center;position:relative;width:8px}.pulsing-dot:after{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.pulsing-dot:after,.pulsing-dot:before{background:#10b981;border-radius:50%;content:\"\";height:100%;position:absolute;width:100%}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.indicator-text{color:#047857;font-size:11px;font-weight:800;letter-spacing:.5px;line-height:1;text-transform:uppercase}.lobby-status-section{display:flex;flex-direction:column;margin-bottom:24px}.lobby-status{background:#fff;border-radius:4px;color:#000;font-size:10px;font-weight:600;line-height:1.2;padding:6px 10px;text-transform:uppercase}.lobby-status.active{background:#155dfc;color:#fff}.lobby-status.complete{background:#10b981;color:#fff}.lobby-status-wp{align-items:center;display:flex;flex-wrap:wrap;gap:10px}.lobby-number{backdrop-filter:blur(3px);background:hsla(0,0%,100%,.2);border-radius:4px;box-shadow:0 25px 50px -12px #00000040;font-size:12px;font-weight:700;letter-spacing:1px;line-height:1.2;padding:5px 8px;text-transform:uppercase}.title-wp{margin-top:25px}.title-wp h2{font-size:60px;font-weight:900;line-height:1;margin-bottom:15px}.sub-title{-webkit-text-fill-color:transparent;animation:gradient-flow 4s linear infinite;background-clip:text;-webkit-background-clip:text;background-image:linear-gradient(90deg,#1e293b,#2563eb 25%,#1e293b 50%);background-size:200% auto;color:#1e293b;font-size:16px;font-weight:700;line-height:1.5;margin-bottom:0}@keyframes gradient-flow{0%{background-position:200% 0}to{background-position:-200% 0}}.sub-title.completed{-webkit-text-fill-color:unset;background-clip:unset;-webkit-background-clip:unset;background-image:none;color:#1e293b}.connected-section{backdrop-filter:blur(4px);background:hsla(0,0%,100%,.05);border:1px solid hsla(0,0%,100%,.1);border-radius:24px;box-shadow:0 1px 2px 0 rgba(0,0,0,.05);display:flex;flex-direction:column;gap:20px;margin-top:24px;padding:24px;transition:all .3s ease}.connected-section:hover{background:hsla(0,0%,100%,.1);box-shadow:0 2px 4px 0 rgba(0,0,0,.08)}.link-share-container{display:flex;flex-direction:column}.link-share-wrapper{align-items:center;display:flex;gap:12px;width:100%}.lobby-link-box{align-items:center;backdrop-filter:blur(8px);background-color:hsla(0,0%,100%,.4);border:1px solid hsla(0,0%,100%,.3);border-radius:14px;box-shadow:0 1px 2px 0 rgba(0,0,0,.05);display:flex;flex:1;gap:16px;justify-content:space-between;min-height:50px;padding:16px 20px;transition:all .2s ease}.lobby-link-box:hover{background-color:hsla(0,0%,100%,.5);box-shadow:0 2px 4px 0 rgba(0,0,0,.08)}.lobby-link-text{color:#71717a;font-size:10px;font-weight:700;letter-spacing:.6px;line-height:1.2;margin-bottom:6px;text-transform:uppercase}.copy-link-btn{align-items:center;background:transparent;border:none;border-radius:6px;color:#64748b;cursor:pointer;display:flex;flex-shrink:0;font-size:13px;font-weight:600;gap:6px;justify-content:center;padding:0;transition:all .2s ease;white-space:nowrap}.copy-link-btn:hover{color:#1e293b}.copy-link-btn .copy-text{display:none}.copy-link-btn svg{flex-shrink:0;height:18px;width:18px}@media (min-width:640px){.copy-link-btn .copy-text{display:inline}}.lobby-link-url{color:#1e293b;font-size:15px;font-weight:700;line-height:1.4;word-break:break-all}.link-box-container{flex:1;min-width:0}.share-btn{align-items:center;background:#1e293b;border:none;border-radius:14px;box-shadow:0 2px 4px 0 rgba(0,0,0,.1);color:#fff;cursor:pointer;display:flex;flex-shrink:0;font-size:15px;font-weight:600;gap:8px;height:50px;justify-content:center;min-width:auto;padding:35px 24px;transition:all .2s ease}.share-btn .share-text{color:#fff;display:inline;font-size:15px}.share-btn svg{color:#fff}@media (max-width:640px){.share-btn{font-size:14px;height:50px;padding:0 18px}.share-btn .share-text{color:#fff;display:inline}}.share-btn:hover{background:#0f172a;box-shadow:0 4px 8px 0 rgba(0,0,0,.15);transform:translateY(-1px)}.share-btn:active{transform:scale(.95)}.share-btn svg{flex-shrink:0;height:18px;width:18px}.lobby-offer-box{align-items:center;backdrop-filter:blur(8px);background-color:rgba(59,130,246,.063);border:1px solid rgba(59,130,246,.125);border-radius:1rem;display:flex;gap:30px;margin-top:30px;padding:15px 20px}.offer-box-icon{align-items:center;background:linear-gradient(135deg,#3b82f6,rgba(59,130,246,.867));border-radius:10px;box-shadow:0 10px 15px -3px rgba(59,130,246,.314);color:#fff;cursor:pointer;display:flex;height:56px;justify-content:center;padding:5px;width:56px}.offer-lock-status{align-items:center;display:flex;gap:6px;margin-bottom:2px}.offer-lock-status span{font-size:14px;font-weight:700;line-height:1}.offer-box-content h3{font-size:30px;font-weight:900;line-height:1.2}.cb-lobby-top{display:grid;gap:100px;grid-template-columns:7fr 5fr}.group-info{backdrop-filter:blur(8px);background-color:color-mix(in oklab,#fff 5%,transparent);border-radius:20px;box-shadow:0 10px 15px -3px #0000001a;padding:30px}.progress-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:14px}.progress-header .title{align-items:center;color:#000;display:flex;font-size:14px;font-weight:900;justify-content:center;letter-spacing:.7px;position:relative}.progress-badge{background:#3b82f6;border-radius:999px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;color:#fff;font-size:13px;font-weight:700;padding:6px 12px}.cb-lobby-modal-container .progress-bar{background:#fff;border-radius:999px;height:14px;overflow:hidden;width:100%}.cb-lobby-modal-container .progress-fill{animation:shimmer 2.7s linear infinite;background:#3b82f6;background-image:linear-gradient(120deg,hsla(0,0%,100%,.15) 25%,hsla(0,0%,100%,.45) 37%,hsla(0,0%,100%,.15) 63%);background-size:200% 100%;border-radius:999px;height:100%;position:relative;transition:width .6s ease;width:0}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.progress-labels{color:#474d56;display:flex;font-size:12px;justify-content:space-between;margin-top:8px}.team-card{margin-top:40px}.team-card .icon-box svg{width:16px}.team-card .team-header{align-items:center;display:flex;justify-content:space-between}.team-card .team-title{align-items:center;display:flex;font-size:14px;font-weight:600;gap:12px;letter-spacing:1px}.team-card .icon-box{align-items:center;background-color:#3b82f6;border-radius:.5rem;box-shadow:0 10px 15px -3px #3b82f64d;color:#fff;display:flex;font-size:18px;height:2rem;justify-content:center;width:2rem}.team-card .team-title span{color:#000;font-size:14px;font-weight:900;letter-spacing:.7px}.team-card .team-count{background-color:#3b82f6;border-radius:9999px;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;color:#fff;font-size:13px;font-weight:700;letter-spacing:.7px;padding:6px 12px}.team-card .team-members{align-items:center;display:flex;gap:14px;justify-content:center;margin-top:20px}.team-card .member{border:3px solid #fff;border-radius:50%;box-shadow:0 4px 12px #0000001a;font-size:22px;height:56px;position:relative;width:56px}.team-card .member,.team-card .member:after{align-items:center;color:#fff;display:flex;justify-content:center}.team-card .member:after{background:#3b82f6;background-image:url(https://cobuy-dev.s3.af-south-1.amazonaws.com/public/sdk/tick-mark-icon.svg);background-position:50%;background-repeat:no-repeat;background-size:75%;border:2px solid #fff;border-radius:50%;bottom:-4px;content:\"\";height:12px;padding:2px;position:absolute;right:-4px;width:12px}.team-card .member.blue{background:#2563eb}.team-card .member.purple{background:#9333ea}.team-card .member.pink{background:#ec4899}.team-card .member.orange{background:#f97316}.team-card .member.empty{background:#f8fafc;color:#9ca3af}.team-card .member.empty:after{display:none}.time-card{align-items:center;backdrop-filter:blur(8px);background:hsla(0,0%,100%,.2);border:1px solid hsla(0,0%,100%,.2);border-radius:20px;display:flex;gap:14px;margin-top:30px;padding:15px}.time-card .time-icon{align-items:center;background:#3b82f6;border-radius:14px;color:#fff;display:flex;flex-shrink:0;font-size:22px;height:48px;justify-content:center;width:48px}.time-card .time-content{display:flex;flex-direction:column}.time-card .time-label{color:#4b5563;font-size:12px;font-weight:600;letter-spacing:.6px}.time-card .time-value{color:#111827;font-size:22px;font-weight:900}.group-info-box{backdrop-filter:blur(8px);background:color-mix(in oklab,#fff 5%,transparent);border:1px solid color-mix(in oklab,#fff 20%,transparent);border-radius:1.5rem;box-shadow:0 10px 15px -3px #0000001a;padding:30px}.offer-lock-status svg{height:16px;width:16px}.cb-lobby-modal-container .offer-box-content .reward-text{background:unset;border:none;color:#000;font-size:14px;line-height:1;margin-top:4px}.progress-header .title:before{background:#3b82f6;border-radius:50%;content:\"\";display:inline-block;height:8px;margin-right:8px;position:relative;width:8px}.live-activity-wrapper{backdrop-filter:blur(8px);background-color:color-mix(in oklab,#fff 80%,transparent);border-radius:18px;box-shadow:0 30px 80px rgba(0,0,0,.15);padding:16px}.live-activity-header{display:flex;gap:10px;justify-content:space-between;margin-bottom:12px}.live-activity-header .title{align-items:center;color:#000;display:flex;font-size:14px;font-weight:600;font-weight:900;gap:8px;letter-spacing:.7px}.live-activity-header .dot{background:#3b82f6;border-radius:50%;height:8px;position:relative;width:8px}.live-activity-header .dot:after{animation:livePulse 1.6s ease-out infinite;background:rgba(59,130,246,.6);border-radius:50%;content:\"\";inset:0;position:absolute}@keyframes livePulse{0%{opacity:.8;transform:scale(1)}70%{opacity:0;transform:scale(2.4)}to{opacity:0}}.activity-stats{align-items:center;display:flex;gap:8px}.activity-stats-badge{background-color:rgba(59,130,246,.082);border-radius:999px;color:#3b82f6;font-size:12px;font-weight:500;line-height:1;padding:4px 10px}.activity-stats-badge.light{background-color:#f9f3f4;color:#45556c}.activity-list{height:104px;overflow:hidden;position:relative}.activity-card{align-items:center;background:linear-gradient(90deg,#fff,#f2f6ff);border:1px solid #dbeafe;border-radius:14px;display:flex;gap:10px;height:58px;inset:0;padding:12px 10px;position:absolute;transition:transform .6s cubic-bezier(.22,.61,.36,1),opacity .6s}.activity-card .text{font-size:14px}.activity-card .avatar{align-items:center;border-radius:50%;color:#fff;display:flex;flex:0 0 30px;height:30px;justify-content:center;width:30px}.activity-card .pink{background:linear-gradient(135deg,#ec4899,#f472b6)}.activity-card .purple{background:linear-gradient(135deg,#8b5cf6,#a78bfa)}.activity-card .blue{background:linear-gradient(135deg,#3b82f6,#60a5fa)}.activity-card .green{background:linear-gradient(135deg,#10b981,#34d399)}.activity-card .orange{background:linear-gradient(135deg,#f97316,#fb923c)}.activity-card .text p{color:#6b7280;font-size:12px;margin:2px 0 0}.activity-card .time{color:#94a3b8;font-size:10px;margin-left:auto}.lobby-activity-wp{backdrop-filter:blur(12px);background-color:hsla(0,0%,100%,.4);border:1px solid hsla(0,0%,100%,.2);border-radius:25px;box-shadow:0 1px 3px 0 #0000001a;margin-top:50px;padding:8px}.lobby-close-icon{align-items:center;background-color:color-mix(in oklab,#000 80%,transparent);border-radius:50%;color:#fff;cursor:pointer;display:flex;height:34px;justify-content:center;position:fixed;right:30px;top:30px;width:34px;z-index:99}.lobby-close-icon svg{height:20px;width:20px}@media screen and (max-width:1600px){.cb-lobby-main{border-radius:30px;padding:50px}.title-wp h2{font-size:48px;margin-bottom:12px}.sub-title{font-size:16px}.title-wp{margin-top:20px}.lobby-link-section{margin-top:30px}.offer-box-content h3{font-size:26px}.lobby-offer-box{gap:24px;padding:15px}.cb-lobby-top{gap:80px}.group-info-box{border-radius:20px;padding:22px}.team-card{margin-top:30px}.team-card .team-members{margin-top:12px}.lobby-activity-wp{margin-top:40px}.lobby-close-icon{height:30px;top:20px;width:30px}}@media screen and (max-width:1280px){.cb-lobby-main,.cb-lobby-main-wp{padding:40px}.title-wp h2{font-size:42px}.cb-lobby-top{gap:60px}}@media screen and (max-width:1120px){.title-wp h2{font-size:38px}}@media screen and (max-width:991px){.cb-lobby-top{gap:30px;grid-template-columns:1fr}.cb-lobby-main{border-radius:15px;padding:30px}.cb-lobby-main-wp{padding:30px}.lobby-close-icon{right:20px}.lobby-link-box{border-radius:10px}.share-btn{border-radius:8px}.offer-box-content h3{font-size:24px}.lobby-activity-wp,.lobby-offer-box,.time-card{border-radius:15px;margin-top:20px}.live-activity-wrapper{border-radius:10px}.group-info-box{border-radius:15px}}@media screen and (max-width:575px){.cb-lobby-main,.cb-lobby-main-wp{padding:20px}.title-wp h2{font-size:30px}.lobby-link-text{font-size:11px;margin-bottom:4px}.lobby-link-url{font-size:14px}.lobby-link-box{min-height:48px;padding:12px 16px}.link-share-wrapper{gap:10px}.share-btn{border-radius:8px;font-size:13px;height:48px;min-width:auto;padding:0 14px}.lobby-offer-box{padding:10px}.offer-box-content h3{font-size:20px}.offer-box-content .reward-text{font-size:13px}.group-info-box{padding:13px}.progress-header .title,.team-card .team-title span{font-size:13px}.team-card .member{height:40px;width:40px}.team-card .member:after{height:8px;width:8px}.team-card .team-members{gap:10px;margin-top:12px}.time-card{padding:13px}.team-card{margin-top:22px}.activity-card .text{font-size:12px}.activity-card .text p{font-size:11px}.live-activity-header .title{font-size:12px}.time-card .time-value{font-size:20px}}@media screen and (max-width:480px){.cb-lobby-main-wp{padding:20px 12px}.cb-lobby-main{padding:15px 12px}.lobby-status{font-size:9px}.lobby-number{font-size:10px}.title-wp h2{font-size:28px;margin-bottom:7px}.sub-title{font-size:14px}.lobby-link-section{gap:10px;margin-top:20px}.lobby-offer-box{gap:12px}.offer-box-icon{height:45px;width:45px}.offer-box-content .reward-text,.offer-lock-status span{font-size:12px}.cb-lobby-top{gap:20px}.lobby-close-icon{right:10px;top:10px}.live-activity-header{flex-direction:column;gap:5px}.activity-card{border-radius:9px;padding:6px}}.share-overlay{align-items:center;background:rgba(0,0,0,.45);display:flex;inset:0;justify-content:center;opacity:0;position:fixed;transition:.3s ease;visibility:hidden;z-index:999}.share-overlay.active{opacity:1;visibility:visible}.share-popup{background:#fff;border-radius:18px;max-height:90%;overflow:auto;padding:24px;position:relative;transform:translateY(30px);transition:.35s ease;width:420px}.share-overlay.active .share-popup{transform:translateY(0)}.share-popup .close-btn{align-items:center;background:#f3f4f6;border:none;border-radius:50%;cursor:pointer;display:flex;height:32px;justify-content:center;position:absolute;right:14px;top:14px;transition:.3s;width:32px}.share-popup .close-btn:hover{background:#eeecec}.share-popup h2{font-size:22px;font-weight:600;line-height:1;margin-bottom:10px}.share-popup .subtitle{color:#4a5565;font-size:14px;margin:6px 0 30px}.share-popup .share-grid{display:grid;gap:14px;grid-template-columns:repeat(2,1fr)}.share-popup .share-card{align-items:center;background:#f2f6f9;border-radius:14px;cursor:pointer;display:flex;flex-direction:column;padding:16px;position:relative;text-align:center;transition:.25s ease}.share-popup .share-card:hover{transform:translateY(-3px)}.share-popup .share-card .icon{align-items:center;background:#1877f2;border-radius:50%;color:#fff;display:flex;font-size:30px;height:50px;justify-content:center;margin-bottom:8px;text-align:center;width:50px}.share-popup .share-card span{font-size:13px;font-weight:500}.share-popup .share-card.whatsapp{background:#ecfdf5;border:2px solid #22c55e;color:#22c55e}.share-popup .share-card.whatsapp .icon{background:#22c55e}.share-popup .share-card .badge{background:#2563eb;border-radius:12px;color:#fff;font-size:11px;padding:4px 10px;position:absolute;right:10px;top:-8px}.share-popup .link-box{background:#fbf9fa;border:1px solid #ebe6e7;border-radius:14px;margin-top:18px;padding:14px}.share-popup .link-box label{color:#64748b;font-size:13px}.share-popup .link-row{display:flex;gap:8px;margin-top:6px}.share-popup .link-row input{background:transparent;border:none;color:#334155;flex:1;font-size:13px;letter-spacing:.8px;line-height:1.2;outline:none}.share-popup .link-row button{align-items:center;background:#fff;border:1px solid #ebe6e7;border-radius:10px;cursor:pointer;display:flex;height:35px;justify-content:center;padding:6px 8px;width:35px}.share-popup .success{color:#2563eb;display:block;font-size:12px;margin-top:6px;opacity:0;transition:opacity .3s}.share-popup .success.show{opacity:1}.share-popup .footer-text{color:#64748b;font-size:12px;margin-top:14px;text-align:center}.share-popup .share-card.twitter .icon{background:#000}.share-popup .share-card.facebook .icon{background:#1877f2}.share-popup .share-card.sms .icon{background:#059669}.share-popup .share-card.copied .icon{background:#2563eb}@keyframes entrance-fade-in{0%{opacity:0}to{opacity:1}}@keyframes entrance-scale-in{0%{opacity:0;transform:scale(.9) translateY(20px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes entrance-text-fade-in{0%{opacity:0}to{opacity:1}}@keyframes exit-blur-scale{0%{filter:blur(0);opacity:1;transform:scale(1)}to{filter:blur(10px);opacity:0;transform:scale(1.1)}}@keyframes pulse-dot{0%,to{opacity:1}50%{opacity:.5}}.entrance-animation-overlay{align-items:center;animation:entrance-fade-in .8s ease-in-out forwards;background-color:#0f172a;color:#fff;display:flex;flex-direction:column;inset:0;justify-content:center;position:fixed;z-index:9999}.entrance-animation-overlay.exit{animation:exit-blur-scale .8s ease-in-out forwards}.entrance-content{align-items:center;animation:entrance-scale-in .5s ease-out .2s both;display:flex;flex-direction:column}.entrance-icon-box{align-items:center;background:#2563eb;border-radius:16px;box-shadow:0 10px 25px rgba(37,99,235,.2);display:flex;height:64px;justify-content:center;margin-bottom:24px;width:64px}.entrance-icon-box svg{color:#fff;height:32px;width:32px}.entrance-title{font-size:32px;font-weight:700;letter-spacing:-.5px;margin-bottom:12px}@media (min-width:768px){.entrance-title{font-size:48px}}.entrance-message{align-items:center;animation:entrance-text-fade-in .5s ease-out .5s both;color:#60a5fa;display:flex;font-size:18px;font-weight:500;gap:8px}.entrance-pulse-dot{animation:pulse-dot 1.5s ease-in-out infinite;background-color:#60a5fa;border-radius:50%;height:8px;width:8px}";
1294
1545
  styleInject(css_248z,{"insertAt":"bottom"});
1295
1546
 
1296
1547
  /**
@@ -1326,6 +1577,7 @@ class LobbyModal {
1326
1577
  this.handleSocketGroupUpdate = (event) => {
1327
1578
  var _a, _b, _c, _d, _e, _f, _g;
1328
1579
  const detail = event.detail || {};
1580
+ console.log("group completed", event, detail);
1329
1581
  const normalizeId = (id) => {
1330
1582
  if (id === undefined || id === null)
1331
1583
  return null;
@@ -1395,6 +1647,14 @@ class LobbyModal {
1395
1647
  status: isComplete ? "complete" : "active",
1396
1648
  isLocked: !isComplete,
1397
1649
  };
1650
+ // Add offline redemption data if available and group is complete
1651
+ if (isComplete) {
1652
+ const offlineRedemption = detail.offline_redemption;
1653
+ if (offlineRedemption && isValidOfflineRedemption(offlineRedemption)) {
1654
+ updatePayload.offlineRedemption = offlineRedemption;
1655
+ this.logger.info("Offline redemption data received in socket event");
1656
+ }
1657
+ }
1398
1658
  this.updateData(updatePayload);
1399
1659
  // Also update team card to reflect new member count
1400
1660
  this.updateTeamCard(participants, max);
@@ -1412,6 +1672,7 @@ class LobbyModal {
1412
1672
  currentMembers: data.currentMembers,
1413
1673
  totalMembers: data.totalMembers,
1414
1674
  timeLeft: data.timeLeft,
1675
+ offlineRedemption: data.offlineRedemption,
1415
1676
  });
1416
1677
  this.data = Object.assign({ groupNumber: "1000", status: "active", progress: 80, currentMembers: 4, totalMembers: 5, timeLeft: 1390, discount: "20% OFF", isLocked: true, activities: this.getDefaultActivities() }, data);
1417
1678
  // Normalize optional flags so undefined values don't override defaults
@@ -1442,7 +1703,7 @@ class LobbyModal {
1442
1703
  return true;
1443
1704
  }
1444
1705
  getRewardText(isLocked) {
1445
- return isLocked ? "🔓 Complete group to unlock reward." : "🎉 Reward unlocked!";
1706
+ return isLocked ? "🔓 Complete group to unlock discount." : "🎉 Discount unlocked!";
1446
1707
  }
1447
1708
  getTitleText(data) {
1448
1709
  var _a, _b;
@@ -1451,19 +1712,19 @@ class LobbyModal {
1451
1712
  const unlocked = !this.computeIsLocked(data);
1452
1713
  if (unlocked) {
1453
1714
  return {
1454
- title: "Reward unlocked!",
1455
- subtitle: "Group filled — enjoy your reward.",
1715
+ title: "Discount unlocked!",
1716
+ subtitle: "Group filled — enjoy your discount.",
1456
1717
  };
1457
1718
  }
1458
1719
  if (remaining <= 1) {
1459
1720
  return {
1460
1721
  title: "Just one more!",
1461
- subtitle: "Invite a friend to unlock your reward instantly.",
1722
+ subtitle: "Invite a friend to unlock your discount instantly.",
1462
1723
  };
1463
1724
  }
1464
1725
  return {
1465
1726
  title: "Almost there!",
1466
- subtitle: `Just ${remaining} more ${remaining === 1 ? "member" : "members"} — your reward unlocks instantly.`,
1727
+ subtitle: `Just ${remaining} more ${remaining === 1 ? "member" : "members"} — your discount unlocks instantly.`,
1467
1728
  };
1468
1729
  }
1469
1730
  /**
@@ -1513,6 +1774,62 @@ class LobbyModal {
1513
1774
  },
1514
1775
  ];
1515
1776
  }
1777
+ /**
1778
+ * Create entrance animation overlay
1779
+ */
1780
+ createEntranceAnimation() {
1781
+ const overlay = document.createElement("div");
1782
+ overlay.className = "entrance-animation-overlay";
1783
+ overlay.id = "entranceAnimationOverlay";
1784
+ const content = document.createElement("div");
1785
+ content.className = "entrance-content";
1786
+ // Icon box
1787
+ const iconBox = document.createElement("div");
1788
+ iconBox.className = "entrance-icon-box";
1789
+ iconBox.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`;
1790
+ // Title
1791
+ const title = document.createElement("h1");
1792
+ title.className = "entrance-title";
1793
+ title.textContent = "Group Lobby";
1794
+ // Message with pulsing dot
1795
+ const message = document.createElement("p");
1796
+ message.className = "entrance-message";
1797
+ const pulseDot = document.createElement("span");
1798
+ pulseDot.className = "entrance-pulse-dot";
1799
+ const messageText = document.createElement("span");
1800
+ messageText.textContent = "Entering secure session...";
1801
+ message.appendChild(pulseDot);
1802
+ message.appendChild(messageText);
1803
+ content.appendChild(iconBox);
1804
+ content.appendChild(title);
1805
+ content.appendChild(message);
1806
+ overlay.appendChild(content);
1807
+ return overlay;
1808
+ }
1809
+ /**
1810
+ * Show entrance animation and auto-hide after duration
1811
+ */
1812
+ showEntranceAnimation(onComplete) {
1813
+ const overlay = this.createEntranceAnimation();
1814
+ document.body.appendChild(overlay);
1815
+ // After 2.5 seconds, trigger exit animation and remove
1816
+ setTimeout(() => {
1817
+ this.hideEntranceAnimation(overlay, onComplete);
1818
+ }, 2500);
1819
+ }
1820
+ /**
1821
+ * Hide entrance animation with exit effect
1822
+ */
1823
+ hideEntranceAnimation(overlay, onComplete) {
1824
+ overlay.classList.add("exit");
1825
+ // Remove after animation completes (800ms) and call callback
1826
+ setTimeout(() => {
1827
+ overlay.remove();
1828
+ if (onComplete) {
1829
+ onComplete();
1830
+ }
1831
+ }, 800);
1832
+ }
1516
1833
  /**
1517
1834
  * Create the modal DOM structure
1518
1835
  */
@@ -1535,9 +1852,12 @@ class LobbyModal {
1535
1852
  // Top section
1536
1853
  const topSection = this.createTopSection();
1537
1854
  mainContent.appendChild(topSection);
1538
- // Activity section
1539
- const activitySection = this.createActivitySection();
1540
- mainContent.appendChild(activitySection);
1855
+ // Activity section (hidden when offline redemption is present to keep layout compact)
1856
+ const hasOfflineRedemption = this.data.offlineRedemption && isValidOfflineRedemption(this.data.offlineRedemption);
1857
+ if (!hasOfflineRedemption) {
1858
+ const activitySection = this.createActivitySection();
1859
+ mainContent.appendChild(activitySection);
1860
+ }
1541
1861
  mainWrapper.appendChild(mainContent);
1542
1862
  modal.appendChild(background);
1543
1863
  modal.appendChild(mainWrapper);
@@ -1580,9 +1900,9 @@ class LobbyModal {
1580
1900
  // Title
1581
1901
  const titleWp = this.createTitleSection();
1582
1902
  leftWp.appendChild(titleWp);
1583
- // Link
1584
- const linkWp = this.createLinkSection();
1585
- leftWp.appendChild(linkWp);
1903
+ // Connected section (subtitle + link + share)
1904
+ const connectedSection = this.createConnectedSection();
1905
+ leftWp.appendChild(connectedSection);
1586
1906
  // Offer
1587
1907
  const offerBox = this.createOfferSection();
1588
1908
  leftWp.appendChild(offerBox);
@@ -1594,24 +1914,35 @@ class LobbyModal {
1594
1914
  createStatusSection() {
1595
1915
  var _a;
1596
1916
  const statusWp = document.createElement("div");
1597
- statusWp.className = "lobby-status-wp";
1917
+ statusWp.className = "lobby-status-section";
1598
1918
  // Use safe defaults so UI never shows undefined values
1599
1919
  const groupNum = (_a = this.data.groupNumber) !== null && _a !== void 0 ? _a : "1000";
1600
1920
  const status = this.data.status === "complete" ? "complete" : "active";
1601
- const statusLabel = document.createElement("div");
1602
- statusLabel.className = "lobby-status";
1603
- statusLabel.textContent = "Status";
1921
+ // New "You are in the lobby" indicator badge
1922
+ const lobbyIndicator = document.createElement("div");
1923
+ lobbyIndicator.className = "lobby-indicator";
1924
+ const pulsingDot = document.createElement("div");
1925
+ pulsingDot.className = "pulsing-dot";
1926
+ const indicatorText = document.createElement("span");
1927
+ indicatorText.className = "indicator-text";
1928
+ indicatorText.textContent = "YOU ARE IN THE LOBBY";
1929
+ lobbyIndicator.appendChild(pulsingDot);
1930
+ lobbyIndicator.appendChild(indicatorText);
1931
+ // Status row with badge and group number
1932
+ const statusRow = document.createElement("div");
1933
+ statusRow.className = "lobby-status-wp";
1604
1934
  // Render only the current status chip
1605
1935
  const statusChip = document.createElement("div");
1606
1936
  statusChip.className = `lobby-status ${status}`;
1607
1937
  statusChip.id = "lobbyStatusChip";
1608
- statusChip.textContent = status === "complete" ? "Complete" : "Active";
1938
+ statusChip.textContent = status === "complete" ? "COMPLETED" : "ACTIVE";
1609
1939
  const groupNumber = document.createElement("div");
1610
1940
  groupNumber.className = "lobby-number";
1611
1941
  groupNumber.textContent = `Group #${groupNum}`;
1612
- statusWp.appendChild(statusLabel);
1613
- statusWp.appendChild(statusChip);
1614
- statusWp.appendChild(groupNumber);
1942
+ statusRow.appendChild(statusChip);
1943
+ statusRow.appendChild(groupNumber);
1944
+ statusWp.appendChild(lobbyIndicator);
1945
+ statusWp.appendChild(statusRow);
1615
1946
  return statusWp;
1616
1947
  }
1617
1948
  /**
@@ -1624,24 +1955,49 @@ class LobbyModal {
1624
1955
  const title = document.createElement("h2");
1625
1956
  title.id = "lobbyTitleText";
1626
1957
  title.textContent = titleContent.title;
1958
+ titleWp.appendChild(title);
1959
+ return titleWp;
1960
+ }
1961
+ /**
1962
+ * Create connected section (subtitle + link + share)
1963
+ */
1964
+ createConnectedSection() {
1965
+ const connectedSection = document.createElement("div");
1966
+ connectedSection.className = "connected-section";
1967
+ connectedSection.id = "lobbyConnectedSection";
1968
+ const titleContent = this.getTitleText(this.data);
1969
+ // Subtitle
1627
1970
  const subtitle = document.createElement("p");
1628
1971
  subtitle.className = "sub-title";
1629
1972
  subtitle.id = "lobbySubtitleText";
1630
1973
  subtitle.textContent = titleContent.subtitle;
1631
- titleWp.appendChild(title);
1632
- titleWp.appendChild(subtitle);
1633
- return titleWp;
1974
+ connectedSection.appendChild(subtitle);
1975
+ // Check if group is fulfilled and has offline redemption
1976
+ const isComplete = !this.computeIsLocked(this.data);
1977
+ const hasOfflineRedemption = isComplete &&
1978
+ this.data.offlineRedemption &&
1979
+ isValidOfflineRedemption(this.data.offlineRedemption);
1980
+ if (hasOfflineRedemption) {
1981
+ // Show offline redemption view with integrated actions
1982
+ const offlineSection = this.createOfflineRedemptionSection(this.data.offlineRedemption);
1983
+ connectedSection.appendChild(offlineSection);
1984
+ }
1985
+ else {
1986
+ // Show link and share container
1987
+ const linkShareContainer = document.createElement("div");
1988
+ linkShareContainer.className = "link-share-container";
1989
+ const linkWp = this.createLinkSection();
1990
+ linkShareContainer.appendChild(linkWp);
1991
+ connectedSection.appendChild(linkShareContainer);
1992
+ }
1993
+ return connectedSection;
1634
1994
  }
1635
1995
  /**
1636
- * Create link section
1996
+ * Create link section (just the link box and share button, no wrapper)
1637
1997
  */
1638
1998
  createLinkSection() {
1639
- const linkWp = document.createElement("div");
1640
- linkWp.className = "lobby-link-wp";
1641
- linkWp.id = "lobbyLinkSection";
1642
- if (!this.data.isLocked) {
1643
- linkWp.style.display = "none";
1644
- }
1999
+ const linkShareWrapper = document.createElement("div");
2000
+ linkShareWrapper.className = "link-share-wrapper";
1645
2001
  // Link box
1646
2002
  const linkBox = document.createElement("div");
1647
2003
  linkBox.className = "lobby-link-box";
@@ -1656,23 +2012,324 @@ class LobbyModal {
1656
2012
  linkUrl.textContent = this.data.groupLink || `cobuy.group/lobby/${this.data.productId}`;
1657
2013
  linkBoxInr.appendChild(linkText);
1658
2014
  linkBoxInr.appendChild(linkUrl);
1659
- const copyBtn = document.createElement("div");
2015
+ const copyBtn = document.createElement("button");
1660
2016
  copyBtn.className = "copy-link-btn";
1661
- copyBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy w-4 h-4 sm:w-5 sm:h-5"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
2017
+ copyBtn.innerHTML = `<span class="copy-text">Copy</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
1662
2018
  copyBtn.addEventListener("click", () => this.copyLink());
1663
2019
  linkBox.appendChild(linkBoxInr);
1664
2020
  linkBox.appendChild(copyBtn);
1665
2021
  // Share button
1666
- const shareBtn = document.createElement("div");
1667
- shareBtn.className = "share-btn-wp";
1668
- const shareBtnInner = document.createElement("div");
2022
+ const shareBtnInner = document.createElement("button");
1669
2023
  shareBtnInner.className = "share-btn";
1670
- shareBtnInner.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-share w-6 h-6 sm:w-7 sm:h-7"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" x2="12" y1="2" y2="15"></line></svg>`;
2024
+ shareBtnInner.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-share"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" x2="12" y1="2" y2="15"></line></svg><span class="share-text">Share</span>`;
1671
2025
  shareBtnInner.addEventListener("click", () => this.share());
1672
- shareBtn.appendChild(shareBtnInner);
1673
- linkWp.appendChild(linkBox);
1674
- linkWp.appendChild(shareBtn);
1675
- return linkWp;
2026
+ linkShareWrapper.appendChild(linkBox);
2027
+ linkShareWrapper.appendChild(shareBtnInner);
2028
+ return linkShareWrapper;
2029
+ }
2030
+ /**
2031
+ * Create offline redemption section
2032
+ */
2033
+ createOfflineRedemptionSection(offlineRedemption) {
2034
+ const section = document.createElement("div");
2035
+ section.className = "offline-redemption-section";
2036
+ section.id = "lobbyOfflineRedemptionSection";
2037
+ // Top row: QR + Code side by side
2038
+ const topRow = document.createElement("div");
2039
+ topRow.className = "offline-top-row";
2040
+ // QR Code container
2041
+ const qrContainer = document.createElement("div");
2042
+ qrContainer.className = "offline-qr-container";
2043
+ const qrImage = document.createElement("img");
2044
+ qrImage.className = "offline-qr-image";
2045
+ qrImage.alt = "Redemption QR Code";
2046
+ qrImage.src = buildQRUrl(offlineRedemption.qr_code_data);
2047
+ // Handle QR image load errors
2048
+ qrImage.addEventListener("error", () => {
2049
+ qrImage.style.display = "none";
2050
+ const fallbackText = document.createElement("div");
2051
+ fallbackText.className = "offline-qr-fallback";
2052
+ fallbackText.textContent = "QR Code Unavailable";
2053
+ qrContainer.appendChild(fallbackText);
2054
+ });
2055
+ qrContainer.appendChild(qrImage);
2056
+ // Redemption code box
2057
+ const codeBox = document.createElement("div");
2058
+ codeBox.className = "offline-code-box";
2059
+ const codeLabel = document.createElement("p");
2060
+ codeLabel.className = "offline-code-label";
2061
+ codeLabel.textContent = "Redemption Code";
2062
+ const codeValue = document.createElement("div");
2063
+ codeValue.className = "offline-code-value";
2064
+ codeValue.textContent = offlineRedemption.redemption_code;
2065
+ const copyCodeBtn = document.createElement("button");
2066
+ copyCodeBtn.className = "offline-copy-code-btn";
2067
+ copyCodeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
2068
+ copyCodeBtn.addEventListener("click", () => this.copyOfflineRedemptionCode(offlineRedemption.redemption_code, copyCodeBtn));
2069
+ const codeRow = document.createElement("div");
2070
+ codeRow.className = "offline-code-row";
2071
+ codeRow.appendChild(codeValue);
2072
+ codeRow.appendChild(copyCodeBtn);
2073
+ codeBox.appendChild(codeLabel);
2074
+ codeBox.appendChild(codeRow);
2075
+ topRow.appendChild(qrContainer);
2076
+ topRow.appendChild(codeBox);
2077
+ // Expiry info - compact single line
2078
+ const expiryInfo = document.createElement("div");
2079
+ expiryInfo.className = "offline-expiry-info";
2080
+ expiryInfo.innerHTML = `<span class="offline-expiry-label">Valid Until:</span> <span class="offline-expiry-value">${formatExpiryDate(offlineRedemption.offline_expires_at)}</span>`;
2081
+ // Action buttons row
2082
+ const actionsRow = document.createElement("div");
2083
+ actionsRow.className = "offline-actions-row";
2084
+ // Download QR button
2085
+ const downloadQRBtn = document.createElement("button");
2086
+ downloadQRBtn.className = "offline-download-qr-btn";
2087
+ downloadQRBtn.textContent = "Download QR";
2088
+ downloadQRBtn.addEventListener("click", () => this.downloadOfflineQR(offlineRedemption, downloadQRBtn));
2089
+ // Online checkout button
2090
+ const onlineCheckoutBtn = document.createElement("button");
2091
+ onlineCheckoutBtn.className = "offline-online-checkout-btn";
2092
+ onlineCheckoutBtn.textContent = "Checkout Online";
2093
+ onlineCheckoutBtn.addEventListener("click", () => {
2094
+ if (this.data.groupLink) {
2095
+ window.open(this.data.groupLink, "_blank");
2096
+ }
2097
+ });
2098
+ actionsRow.appendChild(downloadQRBtn);
2099
+ actionsRow.appendChild(onlineCheckoutBtn);
2100
+ section.appendChild(topRow);
2101
+ section.appendChild(expiryInfo);
2102
+ section.appendChild(actionsRow);
2103
+ // Inject styles for offline redemption section
2104
+ this.injectOfflineRedemptionStyles();
2105
+ return section;
2106
+ }
2107
+ /**
2108
+ * Handle copying offline redemption code
2109
+ */
2110
+ async copyOfflineRedemptionCode(code, button) {
2111
+ const originalContent = button.innerHTML;
2112
+ const success = await copyRedemptionCode(code);
2113
+ if (success) {
2114
+ button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
2115
+ setTimeout(() => {
2116
+ button.innerHTML = originalContent;
2117
+ }, 2000);
2118
+ }
2119
+ }
2120
+ /**
2121
+ * Handle downloading offline QR code
2122
+ */
2123
+ async downloadOfflineQR(offlineRedemption, button) {
2124
+ const originalText = button.textContent;
2125
+ button.disabled = true;
2126
+ button.textContent = "Downloading...";
2127
+ const success = await downloadQRCode(buildQRUrl(offlineRedemption.qr_code_data), "redemption-qr.png");
2128
+ button.textContent = success ? "Downloaded!" : "Failed";
2129
+ setTimeout(() => {
2130
+ button.textContent = originalText;
2131
+ button.disabled = false;
2132
+ }, 2000);
2133
+ }
2134
+ /**
2135
+ * Inject styles for offline redemption section
2136
+ */
2137
+ injectOfflineRedemptionStyles() {
2138
+ if (document.getElementById("lobby-offline-redemption-styles")) {
2139
+ return;
2140
+ }
2141
+ const style = document.createElement("style");
2142
+ style.id = "lobby-offline-redemption-styles";
2143
+ style.textContent = `
2144
+ .offline-redemption-section {
2145
+ display: flex;
2146
+ flex-direction: column;
2147
+ gap: 14px;
2148
+ padding: 16px;
2149
+ background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
2150
+ border: 1.5px solid #e5e7eb;
2151
+ border-radius: 12px;
2152
+ margin-top: 12px;
2153
+ }
2154
+
2155
+ .offline-top-row {
2156
+ display: flex;
2157
+ gap: 14px;
2158
+ align-items: stretch;
2159
+ }
2160
+
2161
+ .offline-qr-container {
2162
+ flex: 0 0 120px;
2163
+ display: flex;
2164
+ justify-content: center;
2165
+ align-items: center;
2166
+ background: white;
2167
+ padding: 10px;
2168
+ border-radius: 8px;
2169
+ border: 1.5px solid #e5e7eb;
2170
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
2171
+ }
2172
+
2173
+ .offline-qr-image {
2174
+ max-width: 100px;
2175
+ max-height: 100px;
2176
+ width: auto;
2177
+ height: auto;
2178
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.08));
2179
+ }
2180
+
2181
+ .offline-qr-fallback {
2182
+ color: #9ca3af;
2183
+ font-size: 11px;
2184
+ text-align: center;
2185
+ }
2186
+
2187
+ .offline-code-box {
2188
+ flex: 1;
2189
+ display: flex;
2190
+ flex-direction: column;
2191
+ gap: 8px;
2192
+ justify-content: center;
2193
+ }
2194
+
2195
+ .offline-code-row {
2196
+ position: relative;
2197
+ display: flex;
2198
+ align-items: center;
2199
+ }
2200
+
2201
+ .offline-code-label {
2202
+ margin: 0;
2203
+ font-size: 11px;
2204
+ font-weight: 700;
2205
+ color: #1f2937;
2206
+ text-transform: uppercase;
2207
+ letter-spacing: 0.5px;
2208
+ }
2209
+
2210
+ .offline-code-value {
2211
+ font-family: 'Courier New', monospace;
2212
+ font-size: 14px;
2213
+ font-weight: 700;
2214
+ color: #111827;
2215
+ padding: 10px 40px 10px 12px;
2216
+ background: white;
2217
+ border: 1.5px solid #d1d5db;
2218
+ border-radius: 8px;
2219
+ text-align: center;
2220
+ letter-spacing: 1.5px;
2221
+ position: relative;
2222
+ display: flex;
2223
+ align-items: center;
2224
+ justify-content: center;
2225
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
2226
+ transition: all 0.2s;
2227
+ }
2228
+
2229
+ .offline-code-row .offline-code-value {
2230
+ width: 100%;
2231
+ }
2232
+
2233
+ .offline-code-value:hover {
2234
+ border-color: #3b82f6;
2235
+ box-shadow: 0 2px 6px rgba(59, 130, 246, 0.1);
2236
+ }
2237
+
2238
+ .offline-copy-code-btn {
2239
+ position: absolute;
2240
+ right: 6px;
2241
+ top: 50%;
2242
+ transform: translateY(-50%);
2243
+ background: none;
2244
+ border: none;
2245
+ padding: 5px 8px;
2246
+ cursor: pointer;
2247
+ color: #6b7280;
2248
+ display: flex;
2249
+ align-items: center;
2250
+ justify-content: center;
2251
+ transition: all 0.2s;
2252
+ border-radius: 4px;
2253
+ }
2254
+
2255
+ .offline-copy-code-btn:hover {
2256
+ color: #3b82f6;
2257
+ background: #eff6ff;
2258
+ }
2259
+
2260
+ .offline-expiry-info {
2261
+ font-size: 11px;
2262
+ color: #6b7280;
2263
+ text-align: center;
2264
+ padding: 6px 0;
2265
+ }
2266
+
2267
+ .offline-expiry-label {
2268
+ font-weight: 700;
2269
+ text-transform: uppercase;
2270
+ letter-spacing: 0.5px;
2271
+ color: #1f2937;
2272
+ }
2273
+
2274
+ .offline-expiry-value {
2275
+ font-weight: 600;
2276
+ color: #374151;
2277
+ }
2278
+
2279
+ .offline-actions-row {
2280
+ display: flex;
2281
+ gap: 10px;
2282
+ }
2283
+
2284
+ .offline-download-qr-btn,
2285
+ .offline-online-checkout-btn {
2286
+ flex: 1;
2287
+ padding: 11px 16px;
2288
+ border: none;
2289
+ border-radius: 8px;
2290
+ font-size: 13px;
2291
+ font-weight: 700;
2292
+ cursor: pointer;
2293
+ transition: all 0.2s;
2294
+ text-transform: uppercase;
2295
+ letter-spacing: 0.4px;
2296
+ }
2297
+
2298
+ .offline-download-qr-btn {
2299
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
2300
+ color: white;
2301
+ box-shadow: 0 2px 6px rgba(59, 130, 246, 0.2);
2302
+ }
2303
+
2304
+ .offline-download-qr-btn:hover:not(:disabled) {
2305
+ background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
2306
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
2307
+ transform: translateY(-1px);
2308
+ }
2309
+
2310
+ .offline-online-checkout-btn {
2311
+ background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
2312
+ color: white;
2313
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
2314
+ }
2315
+
2316
+ .offline-online-checkout-btn:hover {
2317
+ background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
2318
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
2319
+ transform: translateY(-1px);
2320
+ }
2321
+
2322
+ .offline-download-qr-btn:active:not(:disabled),
2323
+ .offline-online-checkout-btn:active {
2324
+ transform: translateY(0);
2325
+ }
2326
+
2327
+ .offline-download-qr-btn:disabled {
2328
+ opacity: 0.7;
2329
+ cursor: not-allowed;
2330
+ }
2331
+ `;
2332
+ document.head.appendChild(style);
1676
2333
  }
1677
2334
  /**
1678
2335
  * Create offer section
@@ -2311,24 +2968,27 @@ class LobbyModal {
2311
2968
  groupNumber: this.data.groupNumber,
2312
2969
  status: this.data.status,
2313
2970
  });
2314
- this.modalElement = this.createModalStructure();
2315
- document.body.appendChild(this.modalElement);
2316
- // Subscribe to realtime socket events
2317
- this.subscribeToSocketEvents();
2318
- // If socket is available and connected, subscribe to this group
2319
- if (this.socketManager && this.socketManager.isConnected() && this.currentGroupId) {
2320
- this.logger.info(`[LobbyModal] Socket connected, subscribing to group ${this.currentGroupId}`);
2321
- this.socketManager.subscribeToGroup(this.currentGroupId);
2322
- }
2323
- else if (this.socketManager && !this.socketManager.isConnected()) {
2324
- this.logger.warn("[LobbyModal] Socket manager not connected yet");
2325
- }
2326
- // Start timers and animations
2327
- this.startTimer();
2328
- this.startActivityAnimation();
2329
- // Track analytics
2330
- if (this.analyticsClient) ;
2331
- this.logger.info(`Lobby modal opened for product: ${this.data.productId}`);
2971
+ // Show entrance animation, then open modal after animation completes
2972
+ this.showEntranceAnimation(() => {
2973
+ this.modalElement = this.createModalStructure();
2974
+ document.body.appendChild(this.modalElement);
2975
+ // Subscribe to realtime socket events
2976
+ this.subscribeToSocketEvents();
2977
+ // If socket is available and connected, subscribe to this group
2978
+ if (this.socketManager && this.socketManager.isConnected() && this.currentGroupId) {
2979
+ this.logger.info(`[LobbyModal] Socket connected, subscribing to group ${this.currentGroupId}`);
2980
+ this.socketManager.subscribeToGroup(this.currentGroupId);
2981
+ }
2982
+ else if (this.socketManager && !this.socketManager.isConnected()) {
2983
+ this.logger.warn("[LobbyModal] Socket manager not connected yet");
2984
+ }
2985
+ // Start timers and animations
2986
+ this.startTimer();
2987
+ this.startActivityAnimation();
2988
+ // Track analytics
2989
+ if (this.analyticsClient) ;
2990
+ this.logger.info(`Lobby modal opened for product: ${this.data.productId}`);
2991
+ });
2332
2992
  }
2333
2993
  /**
2334
2994
  * Close the modal
@@ -2374,6 +3034,10 @@ class LobbyModal {
2374
3034
  this.updateLockUI(!!this.data.isLocked);
2375
3035
  this.updateTitleUI(this.data);
2376
3036
  this.updateLinkVisibility(!!this.data.isLocked);
3037
+ // Update offline redemption visibility if data changed
3038
+ if (data.offlineRedemption !== undefined || data.status !== undefined) {
3039
+ this.updateOfflineRedemptionVisibility();
3040
+ }
2377
3041
  // Update progress if changed
2378
3042
  if (data.progress !== undefined) {
2379
3043
  const progressFill = document.getElementById("progressFill");
@@ -2448,6 +3112,42 @@ class LobbyModal {
2448
3112
  return;
2449
3113
  linkSection.style.display = isLocked ? "" : "none";
2450
3114
  }
3115
+ /**
3116
+ * Update offline redemption visibility when group is fulfilled
3117
+ */
3118
+ updateOfflineRedemptionVisibility() {
3119
+ const connectedSection = document.getElementById("lobbyConnectedSection");
3120
+ if (!connectedSection)
3121
+ return;
3122
+ const isComplete = !this.computeIsLocked(this.data);
3123
+ const hasOfflineRedemption = this.data.offlineRedemption && isValidOfflineRedemption(this.data.offlineRedemption);
3124
+ // Get existing elements
3125
+ const existingOffline = connectedSection.querySelector(".offline-redemption-section");
3126
+ const existingLink = connectedSection.querySelector(".link-share-container");
3127
+ if (isComplete && hasOfflineRedemption) {
3128
+ // Show offline redemption, hide link/share
3129
+ if (existingLink) {
3130
+ existingLink.remove();
3131
+ }
3132
+ if (!existingOffline) {
3133
+ const offlineSection = this.createOfflineRedemptionSection(this.data.offlineRedemption);
3134
+ connectedSection.appendChild(offlineSection);
3135
+ }
3136
+ }
3137
+ else {
3138
+ // Show link/share, hide offline redemption
3139
+ if (existingOffline) {
3140
+ existingOffline.remove();
3141
+ }
3142
+ if (!existingLink) {
3143
+ const linkShareContainer = document.createElement("div");
3144
+ linkShareContainer.className = "link-share-container";
3145
+ const linkWp = this.createLinkSection();
3146
+ linkShareContainer.appendChild(linkWp);
3147
+ connectedSection.appendChild(linkShareContainer);
3148
+ }
3149
+ }
3150
+ }
2451
3151
  /**
2452
3152
  * Subscribe to socket events
2453
3153
  */
@@ -2484,7 +3184,7 @@ class LobbyModal {
2484
3184
  break;
2485
3185
  case "group:fulfilled":
2486
3186
  emoji = "🎁";
2487
- action = "unlocked the reward – group filled!";
3187
+ action = "unlocked the discount – group filled!";
2488
3188
  break;
2489
3189
  case "group:created":
2490
3190
  emoji = "🛒";
@@ -2552,6 +3252,405 @@ class LobbyModal {
2552
3252
  }
2553
3253
  }
2554
3254
 
3255
+ /**
3256
+ * Offline Redemption Modal
3257
+ * Displays QR code and redemption code for fulfilled groups
3258
+ */
3259
+ class OfflineRedemptionModal {
3260
+ constructor(offlineRedemption, onClose) {
3261
+ this.modalElement = null;
3262
+ this.offlineRedemption = offlineRedemption;
3263
+ this.onClose = onClose;
3264
+ }
3265
+ /**
3266
+ * Open the modal
3267
+ */
3268
+ open() {
3269
+ if (this.modalElement && document.body.contains(this.modalElement)) {
3270
+ return; // Already open
3271
+ }
3272
+ this.modalElement = this.createModalStructure();
3273
+ document.body.appendChild(this.modalElement);
3274
+ // Trigger entrance animation
3275
+ requestAnimationFrame(() => {
3276
+ var _a;
3277
+ (_a = this.modalElement) === null || _a === void 0 ? void 0 : _a.classList.add("offline-redemption-modal-open");
3278
+ });
3279
+ }
3280
+ /**
3281
+ * Close the modal
3282
+ */
3283
+ close() {
3284
+ if (!this.modalElement)
3285
+ return;
3286
+ this.modalElement.classList.remove("offline-redemption-modal-open");
3287
+ // Remove after animation completes
3288
+ setTimeout(() => {
3289
+ var _a;
3290
+ if (this.modalElement && document.body.contains(this.modalElement)) {
3291
+ document.body.removeChild(this.modalElement);
3292
+ this.modalElement = null;
3293
+ }
3294
+ (_a = this.onClose) === null || _a === void 0 ? void 0 : _a.call(this);
3295
+ }, 300);
3296
+ }
3297
+ /**
3298
+ * Create the modal DOM structure
3299
+ */
3300
+ createModalStructure() {
3301
+ const container = document.createElement("div");
3302
+ container.className = "offline-redemption-modal-overlay";
3303
+ // Backdrop
3304
+ const backdrop = document.createElement("div");
3305
+ backdrop.className = "offline-redemption-backdrop";
3306
+ backdrop.addEventListener("click", () => this.close());
3307
+ // Modal content
3308
+ const modal = document.createElement("div");
3309
+ modal.className = "offline-redemption-modal";
3310
+ // Close button
3311
+ const closeBtn = document.createElement("button");
3312
+ closeBtn.className = "offline-redemption-close-btn";
3313
+ closeBtn.setAttribute("aria-label", "Close modal");
3314
+ closeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>`;
3315
+ closeBtn.addEventListener("click", () => this.close());
3316
+ // Header
3317
+ const header = document.createElement("div");
3318
+ header.className = "offline-redemption-header";
3319
+ const title = document.createElement("h2");
3320
+ title.className = "offline-redemption-title";
3321
+ title.textContent = "Redeem In-Store";
3322
+ header.appendChild(title);
3323
+ header.appendChild(closeBtn);
3324
+ // Body
3325
+ const body = document.createElement("div");
3326
+ body.className = "offline-redemption-body";
3327
+ // QR Code section
3328
+ const qrSection = document.createElement("div");
3329
+ qrSection.className = "offline-redemption-qr-section";
3330
+ const qrImage = document.createElement("img");
3331
+ qrImage.className = "offline-redemption-qr-image";
3332
+ qrImage.alt = "Redemption QR Code";
3333
+ qrImage.src = buildQRUrl(this.offlineRedemption.qr_code_data);
3334
+ // QR image fallback handler
3335
+ qrImage.addEventListener("error", () => {
3336
+ qrImage.style.display = "none";
3337
+ const errorMsg = document.createElement("div");
3338
+ errorMsg.className = "offline-redemption-qr-error";
3339
+ errorMsg.textContent = "QR code unavailable";
3340
+ qrSection.appendChild(errorMsg);
3341
+ });
3342
+ qrSection.appendChild(qrImage);
3343
+ // Redemption code section
3344
+ const codeSection = document.createElement("div");
3345
+ codeSection.className = "offline-redemption-code-section";
3346
+ const codeLabel = document.createElement("p");
3347
+ codeLabel.className = "offline-redemption-code-label";
3348
+ codeLabel.textContent = "Redemption Code:";
3349
+ const codeValue = document.createElement("div");
3350
+ codeValue.className = "offline-redemption-code-value";
3351
+ codeValue.textContent = this.offlineRedemption.redemption_code;
3352
+ codeSection.appendChild(codeLabel);
3353
+ codeSection.appendChild(codeValue);
3354
+ // Expiry section
3355
+ const expirySection = document.createElement("div");
3356
+ expirySection.className = "offline-redemption-expiry-section";
3357
+ const expiryLabel = document.createElement("p");
3358
+ expiryLabel.className = "offline-redemption-expiry-label";
3359
+ expiryLabel.textContent = "Valid Until:";
3360
+ const expiryValue = document.createElement("p");
3361
+ expiryValue.className = "offline-redemption-expiry-value";
3362
+ const isValid = isRedemptionValid(this.offlineRedemption.offline_expires_at);
3363
+ expiryValue.textContent = formatExpiryDate(this.offlineRedemption.offline_expires_at);
3364
+ if (!isValid) {
3365
+ expiryValue.classList.add("expired");
3366
+ }
3367
+ expirySection.appendChild(expiryLabel);
3368
+ expirySection.appendChild(expiryValue);
3369
+ body.appendChild(qrSection);
3370
+ body.appendChild(codeSection);
3371
+ body.appendChild(expirySection);
3372
+ // Actions
3373
+ const actions = document.createElement("div");
3374
+ actions.className = "offline-redemption-actions";
3375
+ const downloadBtn = document.createElement("button");
3376
+ downloadBtn.className = "offline-redemption-action-btn download-btn";
3377
+ downloadBtn.textContent = "Download QR";
3378
+ downloadBtn.addEventListener("click", () => this.handleDownloadQR(downloadBtn));
3379
+ const copyBtn = document.createElement("button");
3380
+ copyBtn.className = "offline-redemption-action-btn copy-btn";
3381
+ copyBtn.textContent = "Copy Code";
3382
+ copyBtn.addEventListener("click", () => this.handleCopyCode(copyBtn));
3383
+ actions.appendChild(downloadBtn);
3384
+ actions.appendChild(copyBtn);
3385
+ // Assemble modal
3386
+ modal.appendChild(header);
3387
+ modal.appendChild(body);
3388
+ modal.appendChild(actions);
3389
+ container.appendChild(backdrop);
3390
+ container.appendChild(modal);
3391
+ // Inject styles
3392
+ this.injectStyles();
3393
+ return container;
3394
+ }
3395
+ /**
3396
+ * Handle download QR button click
3397
+ */
3398
+ async handleDownloadQR(button) {
3399
+ const originalText = button.textContent;
3400
+ button.disabled = true;
3401
+ button.textContent = "Downloading...";
3402
+ const success = await downloadQRCode(buildQRUrl(this.offlineRedemption.qr_code_data), "redemption-qr.png");
3403
+ button.textContent = success ? "Downloaded!" : "Download Failed";
3404
+ setTimeout(() => {
3405
+ button.textContent = originalText;
3406
+ button.disabled = false;
3407
+ }, 2000);
3408
+ }
3409
+ /**
3410
+ * Handle copy code button click
3411
+ */
3412
+ async handleCopyCode(button) {
3413
+ const originalText = button.textContent;
3414
+ button.disabled = true;
3415
+ const success = await copyRedemptionCode(this.offlineRedemption.redemption_code);
3416
+ button.textContent = success ? "Copied!" : "Copy Failed";
3417
+ setTimeout(() => {
3418
+ button.textContent = originalText;
3419
+ button.disabled = false;
3420
+ }, 2000);
3421
+ }
3422
+ /**
3423
+ * Inject modal styles
3424
+ */
3425
+ injectStyles() {
3426
+ // Check if styles already injected
3427
+ if (document.getElementById("offline-redemption-modal-styles")) {
3428
+ return;
3429
+ }
3430
+ const style = document.createElement("style");
3431
+ style.id = "offline-redemption-modal-styles";
3432
+ style.textContent = `
3433
+ .offline-redemption-modal-overlay {
3434
+ position: fixed;
3435
+ top: 0;
3436
+ left: 0;
3437
+ right: 0;
3438
+ bottom: 0;
3439
+ display: flex;
3440
+ align-items: center;
3441
+ justify-content: center;
3442
+ z-index: 10000;
3443
+ opacity: 0;
3444
+ transition: opacity 0.3s ease-out;
3445
+ }
3446
+
3447
+ .offline-redemption-modal-overlay.offline-redemption-modal-open {
3448
+ opacity: 1;
3449
+ }
3450
+
3451
+ .offline-redemption-backdrop {
3452
+ position: absolute;
3453
+ top: 0;
3454
+ left: 0;
3455
+ right: 0;
3456
+ bottom: 0;
3457
+ background: rgba(0, 0, 0, 0.5);
3458
+ cursor: pointer;
3459
+ }
3460
+
3461
+ .offline-redemption-modal {
3462
+ position: relative;
3463
+ z-index: 10001;
3464
+ background: white;
3465
+ border-radius: 12px;
3466
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
3467
+ max-width: 400px;
3468
+ width: 90%;
3469
+ max-height: 90vh;
3470
+ overflow-y: auto;
3471
+ animation: slideUp 0.3s ease-out;
3472
+ }
3473
+
3474
+ @keyframes slideUp {
3475
+ from {
3476
+ opacity: 0;
3477
+ transform: translateY(40px);
3478
+ }
3479
+ to {
3480
+ opacity: 1;
3481
+ transform: translateY(0);
3482
+ }
3483
+ }
3484
+
3485
+ .offline-redemption-header {
3486
+ display: flex;
3487
+ justify-content: space-between;
3488
+ align-items: center;
3489
+ padding: 20px;
3490
+ border-bottom: 1px solid #f0f0f0;
3491
+ }
3492
+
3493
+ .offline-redemption-title {
3494
+ margin: 0;
3495
+ font-size: 20px;
3496
+ font-weight: 600;
3497
+ color: #111827;
3498
+ }
3499
+
3500
+ .offline-redemption-close-btn {
3501
+ background: none;
3502
+ border: none;
3503
+ padding: 0;
3504
+ cursor: pointer;
3505
+ display: flex;
3506
+ align-items: center;
3507
+ justify-content: center;
3508
+ color: #6b7280;
3509
+ transition: color 0.2s;
3510
+ }
3511
+
3512
+ .offline-redemption-close-btn:hover {
3513
+ color: #111827;
3514
+ }
3515
+
3516
+ .offline-redemption-close-btn svg {
3517
+ width: 24px;
3518
+ height: 24px;
3519
+ }
3520
+
3521
+ .offline-redemption-body {
3522
+ padding: 24px 20px;
3523
+ display: flex;
3524
+ flex-direction: column;
3525
+ gap: 20px;
3526
+ }
3527
+
3528
+ .offline-redemption-qr-section {
3529
+ display: flex;
3530
+ justify-content: center;
3531
+ background: #f9fafb;
3532
+ padding: 16px;
3533
+ border-radius: 8px;
3534
+ min-height: 200px;
3535
+ align-items: center;
3536
+ }
3537
+
3538
+ .offline-redemption-qr-image {
3539
+ max-width: 100%;
3540
+ max-height: 200px;
3541
+ width: auto;
3542
+ height: auto;
3543
+ }
3544
+
3545
+ .offline-redemption-qr-error {
3546
+ text-align: center;
3547
+ color: #6b7280;
3548
+ font-size: 14px;
3549
+ }
3550
+
3551
+ .offline-redemption-code-section,
3552
+ .offline-redemption-expiry-section {
3553
+ display: flex;
3554
+ flex-direction: column;
3555
+ gap: 8px;
3556
+ }
3557
+
3558
+ .offline-redemption-code-label,
3559
+ .offline-redemption-expiry-label {
3560
+ margin: 0;
3561
+ font-size: 12px;
3562
+ font-weight: 600;
3563
+ color: #6b7280;
3564
+ text-transform: uppercase;
3565
+ letter-spacing: 0.5px;
3566
+ }
3567
+
3568
+ .offline-redemption-code-value {
3569
+ font-family: monospace;
3570
+ font-size: 16px;
3571
+ font-weight: 600;
3572
+ color: #111827;
3573
+ padding: 12px;
3574
+ background: #f9fafb;
3575
+ border: 1px solid #e5e7eb;
3576
+ border-radius: 6px;
3577
+ text-align: center;
3578
+ letter-spacing: 2px;
3579
+ }
3580
+
3581
+ .offline-redemption-expiry-value {
3582
+ margin: 0;
3583
+ font-size: 14px;
3584
+ color: #374151;
3585
+ }
3586
+
3587
+ .offline-redemption-expiry-value.expired {
3588
+ color: #dc2626;
3589
+ font-weight: 600;
3590
+ }
3591
+
3592
+ .offline-redemption-actions {
3593
+ display: flex;
3594
+ gap: 12px;
3595
+ padding: 20px;
3596
+ border-top: 1px solid #f0f0f0;
3597
+ }
3598
+
3599
+ .offline-redemption-action-btn {
3600
+ flex: 1;
3601
+ padding: 12px 16px;
3602
+ border: none;
3603
+ border-radius: 6px;
3604
+ font-size: 14px;
3605
+ font-weight: 600;
3606
+ cursor: pointer;
3607
+ transition: all 0.2s;
3608
+ }
3609
+
3610
+ .download-btn {
3611
+ background: #3b82f6;
3612
+ color: white;
3613
+ }
3614
+
3615
+ .download-btn:hover:not(:disabled) {
3616
+ background: #2563eb;
3617
+ }
3618
+
3619
+ .copy-btn {
3620
+ background: #f3f4f6;
3621
+ color: #111827;
3622
+ border: 1px solid #e5e7eb;
3623
+ }
3624
+
3625
+ .copy-btn:hover:not(:disabled) {
3626
+ background: #e5e7eb;
3627
+ }
3628
+
3629
+ .offline-redemption-action-btn:disabled {
3630
+ opacity: 0.7;
3631
+ cursor: not-allowed;
3632
+ }
3633
+
3634
+ /* Mobile responsiveness */
3635
+ @media (max-width: 480px) {
3636
+ .offline-redemption-modal {
3637
+ width: 95%;
3638
+ max-height: 95vh;
3639
+ }
3640
+
3641
+ .offline-redemption-body {
3642
+ padding: 16px;
3643
+ }
3644
+
3645
+ .offline-redemption-actions {
3646
+ padding: 16px;
3647
+ }
3648
+ }
3649
+ `;
3650
+ document.head.appendChild(style);
3651
+ }
3652
+ }
3653
+
2555
3654
  /// <reference lib="dom" />
2556
3655
  /**
2557
3656
  * Widget state enumeration for tracking render lifecycle
@@ -2586,6 +3685,8 @@ class WidgetRoot {
2586
3685
  this.lastGroupDataRefreshTime = 0;
2587
3686
  this.GROUP_REFRESH_DEBOUNCE = 1000; // 1 second min between refreshes
2588
3687
  this.groupExpiryRefreshTriggered = false; // Track if expiry refresh already triggered for current group
3688
+ this.offlineRedemption = null;
3689
+ this.offlineRedemptionModal = null;
2589
3690
  /** Handle backend fulfillment notifications */
2590
3691
  this.handleGroupFulfilledEvent = (event) => {
2591
3692
  const detail = event.detail;
@@ -2742,11 +3843,22 @@ class WidgetRoot {
2742
3843
  }
2743
3844
  /** Persist fulfilled state, emit callback, and refresh UI */
2744
3845
  processGroupFulfilled(eventData) {
2745
- var _a, _b;
3846
+ var _a, _b, _c, _d;
2746
3847
  this.groupFulfilled = true;
2747
- this.frozenReward = eventData.reward || null;
2748
- this.currentGroupId = eventData.groupId;
2749
- (_b = (_a = this.events) === null || _a === void 0 ? void 0 : _a.onGroupFulfilled) === null || _b === void 0 ? void 0 : _b.call(_a, eventData);
3848
+ // Extract reward from new event structure or fallback to legacy
3849
+ const reward = ((_a = eventData.frozen_reward) === null || _a === void 0 ? void 0 : _a.reward) || eventData.reward || null;
3850
+ this.frozenReward = reward;
3851
+ // Extract group ID from new event structure or fallback to legacy
3852
+ this.currentGroupId = ((_b = eventData.group) === null || _b === void 0 ? void 0 : _b.id) || eventData.groupId || null;
3853
+ // Store offline redemption data if available
3854
+ if (eventData.offline_redemption && isValidOfflineRedemption(eventData.offline_redemption)) {
3855
+ this.offlineRedemption = eventData.offline_redemption;
3856
+ this.logger.info("Offline redemption data stored", {
3857
+ member_id: this.offlineRedemption.member_id,
3858
+ code: this.offlineRedemption.redemption_code,
3859
+ });
3860
+ }
3861
+ (_d = (_c = this.events) === null || _c === void 0 ? void 0 : _c.onGroupFulfilled) === null || _d === void 0 ? void 0 : _d.call(_c, eventData);
2750
3862
  this.renderFulfilledState();
2751
3863
  }
2752
3864
  /** Re-render widget and external containers to reflect fulfillment */
@@ -2861,16 +3973,42 @@ class WidgetRoot {
2861
3973
  rewardCheck.textContent = "✔";
2862
3974
  rewardCheck.style.color = "var(--cobuy-success-color, #10b981)";
2863
3975
  const rewardLabel = document.createElement("span");
2864
- rewardLabel.textContent = rewardText ? `Reward available: ${rewardText}` : "Reward available";
3976
+ rewardLabel.textContent = rewardText
3977
+ ? `Discount available: ${rewardText}`
3978
+ : "Discount available";
2865
3979
  rewardLine.appendChild(rewardCheck);
2866
3980
  rewardLine.appendChild(rewardLabel);
2867
3981
  root.appendChild(title);
2868
3982
  root.appendChild(rewardLine);
2869
- // Create footer with View all Groups link at bottom right
3983
+ // Create footer with action CTAs
2870
3984
  const footer = document.createElement("div");
2871
3985
  footer.style.display = "flex";
2872
3986
  footer.style.justifyContent = "flex-end";
2873
3987
  footer.style.alignItems = "center";
3988
+ footer.style.gap = "12px";
3989
+ // Add "Redeem In-store" CTA if offline redemption is available
3990
+ if (this.offlineRedemption && isValidOfflineRedemption(this.offlineRedemption)) {
3991
+ const redeemLink = document.createElement("button");
3992
+ redeemLink.className = "cobuy-redeem-instore-link";
3993
+ redeemLink.style.background = "none";
3994
+ redeemLink.style.border = "none";
3995
+ redeemLink.style.color = "var(--cobuy-primary-color, #3b82f6)";
3996
+ redeemLink.style.cursor = "pointer";
3997
+ redeemLink.style.fontSize = "12px";
3998
+ redeemLink.style.fontWeight = "600";
3999
+ redeemLink.style.textDecoration = "underline";
4000
+ redeemLink.style.padding = "0";
4001
+ redeemLink.style.transition = "opacity 0.2s";
4002
+ redeemLink.textContent = "Redeem In-store";
4003
+ redeemLink.addEventListener("click", () => this.openOfflineRedemptionModal());
4004
+ redeemLink.addEventListener("mouseover", () => {
4005
+ redeemLink.style.opacity = "0.7";
4006
+ });
4007
+ redeemLink.addEventListener("mouseout", () => {
4008
+ redeemLink.style.opacity = "1";
4009
+ });
4010
+ footer.appendChild(redeemLink);
4011
+ }
2874
4012
  root.appendChild(footer);
2875
4013
  return root;
2876
4014
  }
@@ -3054,6 +4192,12 @@ class WidgetRoot {
3054
4192
  this.currentGroupData = groupData;
3055
4193
  this.currentGroupId = (groupData === null || groupData === void 0 ? void 0 : groupData.id) || null;
3056
4194
  this.currentRewardData = rewardData;
4195
+ console.log("group data for product ", options.productId, groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption, isValidOfflineRedemption(groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption));
4196
+ // Extract offline_redemption from API response if available
4197
+ if ((groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption) &&
4198
+ isValidOfflineRedemption(groupData.offline_redemption)) {
4199
+ this.offlineRedemption = groupData.offline_redemption;
4200
+ }
3057
4201
  // Check if group is already fulfilled (full) on initial load
3058
4202
  if (groupData) {
3059
4203
  const participants = Number(groupData.participants_count || 0);
@@ -3422,6 +4566,7 @@ class WidgetRoot {
3422
4566
  createWidget(rewardData, container, options) {
3423
4567
  const isFulfilled = this.groupFulfilled;
3424
4568
  const activeReward = this.frozenReward || (rewardData === null || rewardData === void 0 ? void 0 : rewardData.reward) || null;
4569
+ this.logger.info(`activeReward: ${JSON.stringify(activeReward)}`);
3425
4570
  const wrapper = document.createElement("div");
3426
4571
  wrapper.className = "cobuy-widget";
3427
4572
  wrapper.style.display = "grid";
@@ -3461,28 +4606,29 @@ class WidgetRoot {
3461
4606
  rewardLine.style.opacity = "0";
3462
4607
  rewardLine.style.animation =
3463
4608
  "cobuy-fadeIn var(--cobuy-animation-duration, 300ms) var(--cobuy-animation-easing, ease-out) forwards";
3464
- if (isFulfilled) {
3465
- const rewardText = this.formatRewardText(activeReward);
3466
- rewardLine.textContent = rewardText
3467
- ? `Reward available: ${rewardText}`
3468
- : "Reward available for this group";
3469
- rewardLine.setAttribute("aria-label", rewardText ? `Reward locked in: ${rewardText}` : "Reward available for this group");
3470
- rewardLine.title = rewardLine.textContent;
3471
- }
3472
- else if (activeReward) {
3473
- const rewardText = this.formatRewardText(activeReward);
3474
- rewardLine.textContent = rewardText
3475
- ? `Save up to ${rewardText} with CoBuy`
3476
- : "CoBuy reward available";
3477
- rewardLine.setAttribute("aria-label", `Eligible for CoBuy reward: ${rewardLine.textContent}`);
3478
- rewardLine.title = rewardLine.textContent;
3479
- }
3480
- else {
3481
- rewardLine.textContent = "CoBuy offer loading or unavailable";
3482
- rewardLine.setAttribute("aria-label", "CoBuy offer loading or unavailable");
3483
- rewardLine.title = "CoBuy offer loading or unavailable";
3484
- rewardLine.style.color = "#6b7280";
3485
- }
4609
+ // if (isFulfilled) {
4610
+ // const rewardText = this.formatRewardText(activeReward);
4611
+ // rewardLine.textContent = rewardText
4612
+ // ? `Reward available: ${rewardText}`
4613
+ // : "Reward available for this group";
4614
+ // rewardLine.setAttribute(
4615
+ // "aria-label",
4616
+ // rewardText ? `Reward locked in: ${rewardText}` : "Reward available for this group",
4617
+ // );
4618
+ // rewardLine.title = rewardLine.textContent;
4619
+ // } else if (activeReward) {
4620
+ // const rewardText = this.formatRewardText(activeReward);
4621
+ // rewardLine.textContent = rewardText
4622
+ // ? `Save up to ${rewardText} with CoBuy`
4623
+ // : "CoBuy reward available";
4624
+ // rewardLine.setAttribute("aria-label", `Eligible for CoBuy reward: ${rewardLine.textContent}`);
4625
+ // rewardLine.title = rewardLine.textContent;
4626
+ // } else {
4627
+ // rewardLine.textContent = "CoBuy offer loading or unavailable";
4628
+ // rewardLine.setAttribute("aria-label", "CoBuy offer loading or unavailable");
4629
+ // rewardLine.title = "CoBuy offer loading or unavailable";
4630
+ // rewardLine.style.color = "#6b7280";
4631
+ // }
3486
4632
  sections.reward = rewardLine;
3487
4633
  // Button - semantic button element with accessibility
3488
4634
  const button = document.createElement("button");
@@ -3713,6 +4859,12 @@ class WidgetRoot {
3713
4859
  }
3714
4860
  groupJoinData = joinResponse.data;
3715
4861
  this.logger.info("Successfully joined group", groupJoinData);
4862
+ this.offlineRedemption =
4863
+ groupJoinData &&
4864
+ groupJoinData.offline_redemption &&
4865
+ isValidOfflineRedemption(groupJoinData.offline_redemption)
4866
+ ? groupJoinData.offline_redemption
4867
+ : null;
3716
4868
  // Trigger invite tracking before opening lobby (global for product)
3717
4869
  try {
3718
4870
  const inviteResponse = await this.apiClient.inviteToGroup(this.currentGroupId, "copy_link");
@@ -3758,6 +4910,13 @@ class WidgetRoot {
3758
4910
  const isGroupFulfilled = groupJoinData.group.participants_count >= groupJoinData.group.max_participants;
3759
4911
  const groupLinkFromInvite = inviteData === null || inviteData === void 0 ? void 0 : inviteData.invite_url;
3760
4912
  const shareMessageFromInvite = inviteData === null || inviteData === void 0 ? void 0 : inviteData.share_message;
4913
+ console.log("offlien dataaa", groupJoinData.offline_redemption, isValidOfflineRedemption(groupJoinData.offline_redemption));
4914
+ // Extract offline_redemption from join response if available
4915
+ const offlineRedemptionFromJoin = groupJoinData.offline_redemption &&
4916
+ isValidOfflineRedemption(groupJoinData.offline_redemption)
4917
+ ? groupJoinData.offline_redemption
4918
+ : undefined;
4919
+ this.offlineRedemption = offlineRedemptionFromJoin || null;
3761
4920
  // Prepare modal data with real values from join response
3762
4921
  cobuySDK.openModal({
3763
4922
  productId,
@@ -3776,6 +4935,7 @@ class WidgetRoot {
3776
4935
  groupLink: groupLinkFromInvite || `https://cobuy.group/lobby/${groupJoinData.group.id}`,
3777
4936
  shareMessage: shareMessageFromInvite,
3778
4937
  isLocked: !isGroupFulfilled,
4938
+ offlineRedemption: offlineRedemptionFromJoin,
3779
4939
  activities: [
3780
4940
  {
3781
4941
  emoji: "👤",
@@ -3825,6 +4985,20 @@ class WidgetRoot {
3825
4985
  }
3826
4986
  }
3827
4987
  }
4988
+ /**
4989
+ * Open offline redemption modal when user clicks "Redeem In-store"
4990
+ */
4991
+ openOfflineRedemptionModal() {
4992
+ if (!this.offlineRedemption || !isValidOfflineRedemption(this.offlineRedemption)) {
4993
+ this.logger.warn("Offline redemption data not available");
4994
+ return;
4995
+ }
4996
+ this.logger.info("Opening offline redemption modal");
4997
+ this.offlineRedemptionModal = new OfflineRedemptionModal(this.offlineRedemption, () => {
4998
+ this.offlineRedemptionModal = null;
4999
+ });
5000
+ this.offlineRedemptionModal.open();
5001
+ }
3828
5002
  /**
3829
5003
  * Emit checkout intent without performing navigation
3830
5004
  */
@@ -3883,13 +5057,13 @@ class WidgetRoot {
3883
5057
  const reward = this.frozenReward || (rewardData === null || rewardData === void 0 ? void 0 : rewardData.reward);
3884
5058
  if (this.groupFulfilled) {
3885
5059
  const rewardText = this.formatRewardText(reward);
3886
- return rewardText ? `Reward available: ${rewardText}` : "Reward available for this group";
5060
+ return rewardText ? `Discount available: ${rewardText}` : "Discount available for this group";
3887
5061
  }
3888
5062
  if (!reward) {
3889
- return "Join with CoBuy to unlock rewards";
5063
+ return "Join with CoBuy to unlock discount";
3890
5064
  }
3891
5065
  const rewardText = this.formatRewardText(reward);
3892
- return rewardText ? `Save up to ${rewardText} with CoBuy` : "CoBuy reward available";
5066
+ return rewardText ? `Save up to ${rewardText} with CoBuy` : "CoBuy discount available";
3893
5067
  }
3894
5068
  /**
3895
5069
  * Public hook to request a realtime refresh from external callers
@@ -4487,7 +5661,7 @@ class AnalyticsClient {
4487
5661
  */
4488
5662
  async trackCtaClick(productId) {
4489
5663
  const event = {
4490
- event: "cta_clicked",
5664
+ event: "CTA_CLICKED",
4491
5665
  productId,
4492
5666
  timestamp: new Date().toISOString(),
4493
5667
  sessionId: this.sessionId,
@@ -4531,6 +5705,28 @@ class AnalyticsClient {
4531
5705
  throw error;
4532
5706
  }
4533
5707
  }
5708
+ /**
5709
+ * Track page view event
5710
+ */
5711
+ async trackPageView() {
5712
+ const event = {
5713
+ event: "PAGE_VIEW",
5714
+ timestamp: new Date().toISOString(),
5715
+ context: {
5716
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
5717
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
5718
+ sdkVersion: this.sdkVersion,
5719
+ },
5720
+ };
5721
+ try {
5722
+ await this.sendEvent(event);
5723
+ this.logger.info("[Analytics] Page view tracked");
5724
+ }
5725
+ catch (error) {
5726
+ // Log but don't throw - analytics failure should not break UX
5727
+ this.logger.error("[Analytics] Failed to track page view", error);
5728
+ }
5729
+ }
4534
5730
  /**
4535
5731
  * Track custom event (extensible for future use)
4536
5732
  */
@@ -8718,9 +9914,10 @@ Object.assign(lookup, {
8718
9914
  * Manages a single socket.io connection for the SDK
8719
9915
  */
8720
9916
  class SocketManager {
8721
- constructor(config, sdkVersion) {
9917
+ constructor(config, sdkVersion, sessionId) {
8722
9918
  this.config = config;
8723
9919
  this.sdkVersion = sdkVersion;
9920
+ this.sessionId = sessionId;
8724
9921
  this.socket = null;
8725
9922
  this.logger = new Logger(config.debug);
8726
9923
  }
@@ -8765,6 +9962,7 @@ class SocketManager {
8765
9962
  auth: {
8766
9963
  merchantKey: this.config.merchantKey,
8767
9964
  sdkVersion: this.sdkVersion,
9965
+ sessionId: this.sessionId,
8768
9966
  },
8769
9967
  autoConnect: true,
8770
9968
  });
@@ -8951,7 +10149,9 @@ class CoBuy {
8951
10149
  const ref = window.localStorage.getItem(key);
8952
10150
  if (ref) {
8953
10151
  this.logger.debug(`[SDK] Retrieved checkout reference via prefix: ${key}`);
8954
- const parsedGroupId = key.startsWith(`${basePrefix}_`) ? key.substring(basePrefix.length + 1) : null;
10152
+ const parsedGroupId = key.startsWith(`${basePrefix}_`)
10153
+ ? key.substring(basePrefix.length + 1)
10154
+ : null;
8955
10155
  return { key, checkoutRef: ref, groupId: parsedGroupId };
8956
10156
  }
8957
10157
  }
@@ -9041,7 +10241,7 @@ class CoBuy {
9041
10241
  // Initialize real-time socket connection for public mode
9042
10242
  if (config.authMode === "public" && config.merchantKey) {
9043
10243
  try {
9044
- this.socketManager = new SocketManager(config, SDK_VERSION);
10244
+ this.socketManager = new SocketManager(config, SDK_VERSION, this.sessionId);
9045
10245
  this.socketManager.connect();
9046
10246
  this.socketManager.bindHandlers({
9047
10247
  onGroupFulfilled: (payload) => {
@@ -9083,6 +10283,13 @@ class CoBuy {
9083
10283
  this.logger.warn("[SDK] Failed to initialize sockets", e);
9084
10284
  }
9085
10285
  }
10286
+ // Track page view event after successful initialization
10287
+ if (this.analyticsClient) {
10288
+ this.analyticsClient.trackPageView().catch((error) => {
10289
+ // Non-blocking: Analytics failure should not affect SDK initialization
10290
+ this.logger.warn("[SDK] Failed to track page view", error);
10291
+ });
10292
+ }
9086
10293
  this.logger.info("SDK initialization complete");
9087
10294
  }
9088
10295
  catch (error) {
@@ -9162,6 +10369,7 @@ class CoBuy {
9162
10369
  groupLink: options.groupLink,
9163
10370
  activities: options.activities,
9164
10371
  isLocked: options.isLocked,
10372
+ offlineRedemption: options.offlineRedemption,
9165
10373
  };
9166
10374
  // Create modal instance
9167
10375
  const modal = new LobbyModal(modalData, {