@cshah18/sdk 4.0.0 → 4.2.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.
@@ -504,12 +504,14 @@ class GroupListModal {
504
504
  // Convert API groups to display format
505
505
  const groups = response.data.groups.map((apiGroup) => ({
506
506
  groupId: apiGroup.id,
507
+ name: apiGroup.group_name,
507
508
  timeLabel: this.formatTimeRemaining(apiGroup.timeLeftSeconds),
508
509
  timeLeftSeconds: apiGroup.timeLeftSeconds,
509
510
  joined: apiGroup.participants_count,
510
511
  total: apiGroup.max_participants,
511
512
  isMember: Boolean(apiGroup.is_member),
512
513
  }));
514
+ console.log("groupsss", groups);
513
515
  // If API reports membership, prefer it over cached state
514
516
  const memberGroup = groups.find((g) => g.isMember);
515
517
  if (memberGroup) {
@@ -816,12 +818,12 @@ class GroupListModal {
816
818
  createGroupCard(group) {
817
819
  const card = document.createElement("div");
818
820
  card.className = "cobuy-group-card";
819
- card.setAttribute("data-group-id", group.groupId);
821
+ card.setAttribute("data-group-id", group.name || group.groupId);
820
822
  const header = document.createElement("div");
821
823
  header.className = "cobuy-group-card-header";
822
824
  const groupId = document.createElement("div");
823
825
  groupId.className = "cobuy-group-id";
824
- groupId.textContent = `Group #${group.groupId}`;
826
+ groupId.textContent = `Group #${group.name || group.groupId}`;
825
827
  const timer = document.createElement("div");
826
828
  timer.className = "cobuy-group-timer";
827
829
  timer.innerHTML = `
@@ -1360,6 +1362,158 @@ class GroupListModal {
1360
1362
  }
1361
1363
  }
1362
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
+
1363
1517
  function styleInject(css, ref) {
1364
1518
  if ( ref === void 0 ) ref = {};
1365
1519
  var insertAt = ref.insertAt;
@@ -1387,7 +1541,7 @@ function styleInject(css, ref) {
1387
1541
  }
1388
1542
  }
1389
1543
 
1390
- 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}";
1391
1545
  styleInject(css_248z,{"insertAt":"bottom"});
1392
1546
 
1393
1547
  /**
@@ -1423,6 +1577,7 @@ class LobbyModal {
1423
1577
  this.handleSocketGroupUpdate = (event) => {
1424
1578
  var _a, _b, _c, _d, _e, _f, _g;
1425
1579
  const detail = event.detail || {};
1580
+ console.log("group completed", event, detail);
1426
1581
  const normalizeId = (id) => {
1427
1582
  if (id === undefined || id === null)
1428
1583
  return null;
@@ -1492,6 +1647,14 @@ class LobbyModal {
1492
1647
  status: isComplete ? "complete" : "active",
1493
1648
  isLocked: !isComplete,
1494
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
+ }
1495
1658
  this.updateData(updatePayload);
1496
1659
  // Also update team card to reflect new member count
1497
1660
  this.updateTeamCard(participants, max);
@@ -1509,6 +1672,7 @@ class LobbyModal {
1509
1672
  currentMembers: data.currentMembers,
1510
1673
  totalMembers: data.totalMembers,
1511
1674
  timeLeft: data.timeLeft,
1675
+ offlineRedemption: data.offlineRedemption,
1512
1676
  });
1513
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);
1514
1678
  // Normalize optional flags so undefined values don't override defaults
@@ -1539,7 +1703,7 @@ class LobbyModal {
1539
1703
  return true;
1540
1704
  }
1541
1705
  getRewardText(isLocked) {
1542
- return isLocked ? "🔓 Complete group to unlock reward." : "🎉 Reward unlocked!";
1706
+ return isLocked ? "🔓 Complete group to unlock discount." : "🎉 Discount unlocked!";
1543
1707
  }
1544
1708
  getTitleText(data) {
1545
1709
  var _a, _b;
@@ -1548,19 +1712,19 @@ class LobbyModal {
1548
1712
  const unlocked = !this.computeIsLocked(data);
1549
1713
  if (unlocked) {
1550
1714
  return {
1551
- title: "Reward unlocked!",
1552
- subtitle: "Group filled — enjoy your reward.",
1715
+ title: "Discount unlocked!",
1716
+ subtitle: "Group filled — enjoy your discount.",
1553
1717
  };
1554
1718
  }
1555
1719
  if (remaining <= 1) {
1556
1720
  return {
1557
1721
  title: "Just one more!",
1558
- subtitle: "Invite a friend to unlock your reward instantly.",
1722
+ subtitle: "Invite a friend to unlock your discount instantly.",
1559
1723
  };
1560
1724
  }
1561
1725
  return {
1562
1726
  title: "Almost there!",
1563
- 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.`,
1564
1728
  };
1565
1729
  }
1566
1730
  /**
@@ -1610,6 +1774,62 @@ class LobbyModal {
1610
1774
  },
1611
1775
  ];
1612
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
+ }
1613
1833
  /**
1614
1834
  * Create the modal DOM structure
1615
1835
  */
@@ -1677,9 +1897,9 @@ class LobbyModal {
1677
1897
  // Title
1678
1898
  const titleWp = this.createTitleSection();
1679
1899
  leftWp.appendChild(titleWp);
1680
- // Link
1681
- const linkWp = this.createLinkSection();
1682
- leftWp.appendChild(linkWp);
1900
+ // Connected section (subtitle + link + share)
1901
+ const connectedSection = this.createConnectedSection();
1902
+ leftWp.appendChild(connectedSection);
1683
1903
  // Offer
1684
1904
  const offerBox = this.createOfferSection();
1685
1905
  leftWp.appendChild(offerBox);
@@ -1691,24 +1911,35 @@ class LobbyModal {
1691
1911
  createStatusSection() {
1692
1912
  var _a;
1693
1913
  const statusWp = document.createElement("div");
1694
- statusWp.className = "lobby-status-wp";
1914
+ statusWp.className = "lobby-status-section";
1695
1915
  // Use safe defaults so UI never shows undefined values
1696
1916
  const groupNum = (_a = this.data.groupNumber) !== null && _a !== void 0 ? _a : "1000";
1697
1917
  const status = this.data.status === "complete" ? "complete" : "active";
1698
- const statusLabel = document.createElement("div");
1699
- statusLabel.className = "lobby-status";
1700
- statusLabel.textContent = "Status";
1918
+ // New "You are in the lobby" indicator badge
1919
+ const lobbyIndicator = document.createElement("div");
1920
+ lobbyIndicator.className = "lobby-indicator";
1921
+ const pulsingDot = document.createElement("div");
1922
+ pulsingDot.className = "pulsing-dot";
1923
+ const indicatorText = document.createElement("span");
1924
+ indicatorText.className = "indicator-text";
1925
+ indicatorText.textContent = "YOU ARE IN THE LOBBY";
1926
+ lobbyIndicator.appendChild(pulsingDot);
1927
+ lobbyIndicator.appendChild(indicatorText);
1928
+ // Status row with badge and group number
1929
+ const statusRow = document.createElement("div");
1930
+ statusRow.className = "lobby-status-wp";
1701
1931
  // Render only the current status chip
1702
1932
  const statusChip = document.createElement("div");
1703
1933
  statusChip.className = `lobby-status ${status}`;
1704
1934
  statusChip.id = "lobbyStatusChip";
1705
- statusChip.textContent = status === "complete" ? "Complete" : "Active";
1935
+ statusChip.textContent = status === "complete" ? "COMPLETED" : "ACTIVE";
1706
1936
  const groupNumber = document.createElement("div");
1707
1937
  groupNumber.className = "lobby-number";
1708
1938
  groupNumber.textContent = `Group #${groupNum}`;
1709
- statusWp.appendChild(statusLabel);
1710
- statusWp.appendChild(statusChip);
1711
- statusWp.appendChild(groupNumber);
1939
+ statusRow.appendChild(statusChip);
1940
+ statusRow.appendChild(groupNumber);
1941
+ statusWp.appendChild(lobbyIndicator);
1942
+ statusWp.appendChild(statusRow);
1712
1943
  return statusWp;
1713
1944
  }
1714
1945
  /**
@@ -1721,24 +1952,48 @@ class LobbyModal {
1721
1952
  const title = document.createElement("h2");
1722
1953
  title.id = "lobbyTitleText";
1723
1954
  title.textContent = titleContent.title;
1955
+ titleWp.appendChild(title);
1956
+ return titleWp;
1957
+ }
1958
+ /**
1959
+ * Create connected section (subtitle + link + share)
1960
+ */
1961
+ createConnectedSection() {
1962
+ const connectedSection = document.createElement("div");
1963
+ connectedSection.className = "connected-section";
1964
+ connectedSection.id = "lobbyConnectedSection";
1965
+ const titleContent = this.getTitleText(this.data);
1966
+ // Subtitle
1724
1967
  const subtitle = document.createElement("p");
1725
1968
  subtitle.className = "sub-title";
1726
1969
  subtitle.id = "lobbySubtitleText";
1727
1970
  subtitle.textContent = titleContent.subtitle;
1728
- titleWp.appendChild(title);
1729
- titleWp.appendChild(subtitle);
1730
- return titleWp;
1971
+ connectedSection.appendChild(subtitle);
1972
+ // Check if group is fulfilled and has offline redemption
1973
+ const isComplete = !this.computeIsLocked(this.data);
1974
+ if (isComplete &&
1975
+ this.data.offlineRedemption &&
1976
+ isValidOfflineRedemption(this.data.offlineRedemption)) {
1977
+ // Show offline redemption view instead of link/share
1978
+ const offlineSection = this.createOfflineRedemptionSection(this.data.offlineRedemption);
1979
+ connectedSection.appendChild(offlineSection);
1980
+ }
1981
+ else {
1982
+ // Show link and share container
1983
+ const linkShareContainer = document.createElement("div");
1984
+ linkShareContainer.className = "link-share-container";
1985
+ const linkWp = this.createLinkSection();
1986
+ linkShareContainer.appendChild(linkWp);
1987
+ connectedSection.appendChild(linkShareContainer);
1988
+ }
1989
+ return connectedSection;
1731
1990
  }
1732
1991
  /**
1733
- * Create link section
1992
+ * Create link section (just the link box and share button, no wrapper)
1734
1993
  */
1735
1994
  createLinkSection() {
1736
- const linkWp = document.createElement("div");
1737
- linkWp.className = "lobby-link-wp";
1738
- linkWp.id = "lobbyLinkSection";
1739
- if (!this.data.isLocked) {
1740
- linkWp.style.display = "none";
1741
- }
1995
+ const linkShareWrapper = document.createElement("div");
1996
+ linkShareWrapper.className = "link-share-wrapper";
1742
1997
  // Link box
1743
1998
  const linkBox = document.createElement("div");
1744
1999
  linkBox.className = "lobby-link-box";
@@ -1753,23 +2008,243 @@ class LobbyModal {
1753
2008
  linkUrl.textContent = this.data.groupLink || `cobuy.group/lobby/${this.data.productId}`;
1754
2009
  linkBoxInr.appendChild(linkText);
1755
2010
  linkBoxInr.appendChild(linkUrl);
1756
- const copyBtn = document.createElement("div");
2011
+ const copyBtn = document.createElement("button");
1757
2012
  copyBtn.className = "copy-link-btn";
1758
- 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>`;
2013
+ 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>`;
1759
2014
  copyBtn.addEventListener("click", () => this.copyLink());
1760
2015
  linkBox.appendChild(linkBoxInr);
1761
2016
  linkBox.appendChild(copyBtn);
1762
2017
  // Share button
1763
- const shareBtn = document.createElement("div");
1764
- shareBtn.className = "share-btn-wp";
1765
- const shareBtnInner = document.createElement("div");
2018
+ const shareBtnInner = document.createElement("button");
1766
2019
  shareBtnInner.className = "share-btn";
1767
- 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>`;
2020
+ 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>`;
1768
2021
  shareBtnInner.addEventListener("click", () => this.share());
1769
- shareBtn.appendChild(shareBtnInner);
1770
- linkWp.appendChild(linkBox);
1771
- linkWp.appendChild(shareBtn);
1772
- return linkWp;
2022
+ linkShareWrapper.appendChild(linkBox);
2023
+ linkShareWrapper.appendChild(shareBtnInner);
2024
+ return linkShareWrapper;
2025
+ }
2026
+ /**
2027
+ * Create offline redemption section
2028
+ */
2029
+ createOfflineRedemptionSection(offlineRedemption) {
2030
+ const section = document.createElement("div");
2031
+ section.className = "offline-redemption-section";
2032
+ section.id = "lobbyOfflineRedemptionSection";
2033
+ // QR Code container
2034
+ const qrContainer = document.createElement("div");
2035
+ qrContainer.className = "offline-qr-container";
2036
+ const qrImage = document.createElement("img");
2037
+ qrImage.className = "offline-qr-image";
2038
+ qrImage.alt = "Redemption QR Code";
2039
+ qrImage.src = buildQRUrl(offlineRedemption.qr_code_data);
2040
+ // Handle QR image load errors
2041
+ qrImage.addEventListener("error", () => {
2042
+ qrImage.style.display = "none";
2043
+ const fallbackText = document.createElement("div");
2044
+ fallbackText.className = "offline-qr-fallback";
2045
+ fallbackText.textContent = "QR Code Unavailable";
2046
+ qrContainer.appendChild(fallbackText);
2047
+ });
2048
+ qrContainer.appendChild(qrImage);
2049
+ // Redemption code box
2050
+ const codeBox = document.createElement("div");
2051
+ codeBox.className = "offline-code-box";
2052
+ const codeLabel = document.createElement("p");
2053
+ codeLabel.className = "offline-code-label";
2054
+ codeLabel.textContent = "Redemption Code";
2055
+ const codeValue = document.createElement("div");
2056
+ codeValue.className = "offline-code-value";
2057
+ codeValue.textContent = offlineRedemption.redemption_code;
2058
+ const copyCodeBtn = document.createElement("button");
2059
+ copyCodeBtn.className = "offline-copy-code-btn";
2060
+ 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>`;
2061
+ copyCodeBtn.addEventListener("click", () => this.copyOfflineRedemptionCode(offlineRedemption.redemption_code, copyCodeBtn));
2062
+ codeBox.appendChild(codeLabel);
2063
+ codeBox.appendChild(codeValue);
2064
+ codeBox.appendChild(copyCodeBtn);
2065
+ // Expiry info
2066
+ const expiryInfo = document.createElement("div");
2067
+ expiryInfo.className = "offline-expiry-info";
2068
+ const expiryLabel = document.createElement("p");
2069
+ expiryLabel.className = "offline-expiry-label";
2070
+ expiryLabel.textContent = "Valid Until";
2071
+ const expiryValue = document.createElement("p");
2072
+ expiryValue.className = "offline-expiry-value";
2073
+ expiryValue.textContent = formatExpiryDate(offlineRedemption.offline_expires_at);
2074
+ expiryInfo.appendChild(expiryLabel);
2075
+ expiryInfo.appendChild(expiryValue);
2076
+ // Download QR button
2077
+ const downloadQRBtn = document.createElement("button");
2078
+ downloadQRBtn.className = "offline-download-qr-btn";
2079
+ downloadQRBtn.textContent = "Download QR";
2080
+ downloadQRBtn.addEventListener("click", () => this.downloadOfflineQR(offlineRedemption, downloadQRBtn));
2081
+ section.appendChild(qrContainer);
2082
+ section.appendChild(codeBox);
2083
+ section.appendChild(expiryInfo);
2084
+ section.appendChild(downloadQRBtn);
2085
+ // Inject styles for offline redemption section
2086
+ this.injectOfflineRedemptionStyles();
2087
+ return section;
2088
+ }
2089
+ /**
2090
+ * Handle copying offline redemption code
2091
+ */
2092
+ async copyOfflineRedemptionCode(code, button) {
2093
+ const originalContent = button.innerHTML;
2094
+ const success = await copyRedemptionCode(code);
2095
+ if (success) {
2096
+ 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>`;
2097
+ setTimeout(() => {
2098
+ button.innerHTML = originalContent;
2099
+ }, 2000);
2100
+ }
2101
+ }
2102
+ /**
2103
+ * Handle downloading offline QR code
2104
+ */
2105
+ async downloadOfflineQR(offlineRedemption, button) {
2106
+ const originalText = button.textContent;
2107
+ button.disabled = true;
2108
+ button.textContent = "Downloading...";
2109
+ const success = await downloadQRCode(buildQRUrl(offlineRedemption.qr_code_data), "redemption-qr.png");
2110
+ button.textContent = success ? "Downloaded!" : "Failed";
2111
+ setTimeout(() => {
2112
+ button.textContent = originalText;
2113
+ button.disabled = false;
2114
+ }, 2000);
2115
+ }
2116
+ /**
2117
+ * Inject styles for offline redemption section
2118
+ */
2119
+ injectOfflineRedemptionStyles() {
2120
+ if (document.getElementById("lobby-offline-redemption-styles")) {
2121
+ return;
2122
+ }
2123
+ const style = document.createElement("style");
2124
+ style.id = "lobby-offline-redemption-styles";
2125
+ style.textContent = `
2126
+ .offline-redemption-section {
2127
+ display: flex;
2128
+ flex-direction: column;
2129
+ gap: 16px;
2130
+ padding: 16px 0;
2131
+ }
2132
+
2133
+ .offline-qr-container {
2134
+ display: flex;
2135
+ justify-content: center;
2136
+ background: #f9fafb;
2137
+ padding: 12px;
2138
+ border-radius: 8px;
2139
+ min-height: 160px;
2140
+ align-items: center;
2141
+ }
2142
+
2143
+ .offline-qr-image {
2144
+ max-width: 140px;
2145
+ max-height: 140px;
2146
+ width: auto;
2147
+ height: auto;
2148
+ }
2149
+
2150
+ .offline-qr-fallback {
2151
+ color: #9ca3af;
2152
+ font-size: 12px;
2153
+ text-align: center;
2154
+ }
2155
+
2156
+ .offline-code-box {
2157
+ display: flex;
2158
+ flex-direction: column;
2159
+ gap: 6px;
2160
+ }
2161
+
2162
+ .offline-code-label {
2163
+ margin: 0;
2164
+ font-size: 11px;
2165
+ font-weight: 600;
2166
+ color: #6b7280;
2167
+ text-transform: uppercase;
2168
+ letter-spacing: 0.5px;
2169
+ }
2170
+
2171
+ .offline-code-value {
2172
+ font-family: monospace;
2173
+ font-size: 14px;
2174
+ font-weight: 600;
2175
+ color: #111827;
2176
+ padding: 10px 12px;
2177
+ background: #f9fafb;
2178
+ border: 1px solid #e5e7eb;
2179
+ border-radius: 6px;
2180
+ text-align: center;
2181
+ letter-spacing: 1px;
2182
+ position: relative;
2183
+ display: flex;
2184
+ align-items: center;
2185
+ justify-content: center;
2186
+ }
2187
+
2188
+ .offline-copy-code-btn {
2189
+ position: absolute;
2190
+ right: 8px;
2191
+ background: none;
2192
+ border: none;
2193
+ padding: 4px 8px;
2194
+ cursor: pointer;
2195
+ color: #6b7280;
2196
+ display: flex;
2197
+ align-items: center;
2198
+ transition: color 0.2s;
2199
+ }
2200
+
2201
+ .offline-copy-code-btn:hover {
2202
+ color: #111827;
2203
+ }
2204
+
2205
+ .offline-expiry-info {
2206
+ display: flex;
2207
+ flex-direction: column;
2208
+ gap: 4px;
2209
+ }
2210
+
2211
+ .offline-expiry-label {
2212
+ margin: 0;
2213
+ font-size: 11px;
2214
+ font-weight: 600;
2215
+ color: #6b7280;
2216
+ text-transform: uppercase;
2217
+ letter-spacing: 0.5px;
2218
+ }
2219
+
2220
+ .offline-expiry-value {
2221
+ margin: 0;
2222
+ font-size: 13px;
2223
+ color: #374151;
2224
+ }
2225
+
2226
+ .offline-download-qr-btn {
2227
+ padding: 10px 16px;
2228
+ background: #3b82f6;
2229
+ color: white;
2230
+ border: none;
2231
+ border-radius: 6px;
2232
+ font-size: 13px;
2233
+ font-weight: 600;
2234
+ cursor: pointer;
2235
+ transition: background 0.2s;
2236
+ }
2237
+
2238
+ .offline-download-qr-btn:hover:not(:disabled) {
2239
+ background: #2563eb;
2240
+ }
2241
+
2242
+ .offline-download-qr-btn:disabled {
2243
+ opacity: 0.7;
2244
+ cursor: not-allowed;
2245
+ }
2246
+ `;
2247
+ document.head.appendChild(style);
1773
2248
  }
1774
2249
  /**
1775
2250
  * Create offer section
@@ -2408,24 +2883,27 @@ class LobbyModal {
2408
2883
  groupNumber: this.data.groupNumber,
2409
2884
  status: this.data.status,
2410
2885
  });
2411
- this.modalElement = this.createModalStructure();
2412
- document.body.appendChild(this.modalElement);
2413
- // Subscribe to realtime socket events
2414
- this.subscribeToSocketEvents();
2415
- // If socket is available and connected, subscribe to this group
2416
- if (this.socketManager && this.socketManager.isConnected() && this.currentGroupId) {
2417
- this.logger.info(`[LobbyModal] Socket connected, subscribing to group ${this.currentGroupId}`);
2418
- this.socketManager.subscribeToGroup(this.currentGroupId);
2419
- }
2420
- else if (this.socketManager && !this.socketManager.isConnected()) {
2421
- this.logger.warn("[LobbyModal] Socket manager not connected yet");
2422
- }
2423
- // Start timers and animations
2424
- this.startTimer();
2425
- this.startActivityAnimation();
2426
- // Track analytics
2427
- if (this.analyticsClient) ;
2428
- this.logger.info(`Lobby modal opened for product: ${this.data.productId}`);
2886
+ // Show entrance animation, then open modal after animation completes
2887
+ this.showEntranceAnimation(() => {
2888
+ this.modalElement = this.createModalStructure();
2889
+ document.body.appendChild(this.modalElement);
2890
+ // Subscribe to realtime socket events
2891
+ this.subscribeToSocketEvents();
2892
+ // If socket is available and connected, subscribe to this group
2893
+ if (this.socketManager && this.socketManager.isConnected() && this.currentGroupId) {
2894
+ this.logger.info(`[LobbyModal] Socket connected, subscribing to group ${this.currentGroupId}`);
2895
+ this.socketManager.subscribeToGroup(this.currentGroupId);
2896
+ }
2897
+ else if (this.socketManager && !this.socketManager.isConnected()) {
2898
+ this.logger.warn("[LobbyModal] Socket manager not connected yet");
2899
+ }
2900
+ // Start timers and animations
2901
+ this.startTimer();
2902
+ this.startActivityAnimation();
2903
+ // Track analytics
2904
+ if (this.analyticsClient) ;
2905
+ this.logger.info(`Lobby modal opened for product: ${this.data.productId}`);
2906
+ });
2429
2907
  }
2430
2908
  /**
2431
2909
  * Close the modal
@@ -2471,6 +2949,10 @@ class LobbyModal {
2471
2949
  this.updateLockUI(!!this.data.isLocked);
2472
2950
  this.updateTitleUI(this.data);
2473
2951
  this.updateLinkVisibility(!!this.data.isLocked);
2952
+ // Update offline redemption visibility if data changed
2953
+ if (data.offlineRedemption !== undefined || data.status !== undefined) {
2954
+ this.updateOfflineRedemptionVisibility();
2955
+ }
2474
2956
  // Update progress if changed
2475
2957
  if (data.progress !== undefined) {
2476
2958
  const progressFill = document.getElementById("progressFill");
@@ -2545,6 +3027,42 @@ class LobbyModal {
2545
3027
  return;
2546
3028
  linkSection.style.display = isLocked ? "" : "none";
2547
3029
  }
3030
+ /**
3031
+ * Update offline redemption visibility when group is fulfilled
3032
+ */
3033
+ updateOfflineRedemptionVisibility() {
3034
+ const connectedSection = document.getElementById("lobbyConnectedSection");
3035
+ if (!connectedSection)
3036
+ return;
3037
+ const isComplete = !this.computeIsLocked(this.data);
3038
+ const hasOfflineRedemption = this.data.offlineRedemption && isValidOfflineRedemption(this.data.offlineRedemption);
3039
+ // Get existing elements
3040
+ const existingOffline = connectedSection.querySelector(".offline-redemption-section");
3041
+ const existingLink = connectedSection.querySelector(".link-share-container");
3042
+ if (isComplete && hasOfflineRedemption) {
3043
+ // Show offline redemption, hide link/share
3044
+ if (existingLink) {
3045
+ existingLink.remove();
3046
+ }
3047
+ if (!existingOffline) {
3048
+ const offlineSection = this.createOfflineRedemptionSection(this.data.offlineRedemption);
3049
+ connectedSection.appendChild(offlineSection);
3050
+ }
3051
+ }
3052
+ else {
3053
+ // Show link/share, hide offline redemption
3054
+ if (existingOffline) {
3055
+ existingOffline.remove();
3056
+ }
3057
+ if (!existingLink) {
3058
+ const linkShareContainer = document.createElement("div");
3059
+ linkShareContainer.className = "link-share-container";
3060
+ const linkWp = this.createLinkSection();
3061
+ linkShareContainer.appendChild(linkWp);
3062
+ connectedSection.appendChild(linkShareContainer);
3063
+ }
3064
+ }
3065
+ }
2548
3066
  /**
2549
3067
  * Subscribe to socket events
2550
3068
  */
@@ -2581,7 +3099,7 @@ class LobbyModal {
2581
3099
  break;
2582
3100
  case "group:fulfilled":
2583
3101
  emoji = "🎁";
2584
- action = "unlocked the reward – group filled!";
3102
+ action = "unlocked the discount – group filled!";
2585
3103
  break;
2586
3104
  case "group:created":
2587
3105
  emoji = "🛒";
@@ -2649,6 +3167,405 @@ class LobbyModal {
2649
3167
  }
2650
3168
  }
2651
3169
 
3170
+ /**
3171
+ * Offline Redemption Modal
3172
+ * Displays QR code and redemption code for fulfilled groups
3173
+ */
3174
+ class OfflineRedemptionModal {
3175
+ constructor(offlineRedemption, onClose) {
3176
+ this.modalElement = null;
3177
+ this.offlineRedemption = offlineRedemption;
3178
+ this.onClose = onClose;
3179
+ }
3180
+ /**
3181
+ * Open the modal
3182
+ */
3183
+ open() {
3184
+ if (this.modalElement && document.body.contains(this.modalElement)) {
3185
+ return; // Already open
3186
+ }
3187
+ this.modalElement = this.createModalStructure();
3188
+ document.body.appendChild(this.modalElement);
3189
+ // Trigger entrance animation
3190
+ requestAnimationFrame(() => {
3191
+ var _a;
3192
+ (_a = this.modalElement) === null || _a === void 0 ? void 0 : _a.classList.add("offline-redemption-modal-open");
3193
+ });
3194
+ }
3195
+ /**
3196
+ * Close the modal
3197
+ */
3198
+ close() {
3199
+ if (!this.modalElement)
3200
+ return;
3201
+ this.modalElement.classList.remove("offline-redemption-modal-open");
3202
+ // Remove after animation completes
3203
+ setTimeout(() => {
3204
+ var _a;
3205
+ if (this.modalElement && document.body.contains(this.modalElement)) {
3206
+ document.body.removeChild(this.modalElement);
3207
+ this.modalElement = null;
3208
+ }
3209
+ (_a = this.onClose) === null || _a === void 0 ? void 0 : _a.call(this);
3210
+ }, 300);
3211
+ }
3212
+ /**
3213
+ * Create the modal DOM structure
3214
+ */
3215
+ createModalStructure() {
3216
+ const container = document.createElement("div");
3217
+ container.className = "offline-redemption-modal-overlay";
3218
+ // Backdrop
3219
+ const backdrop = document.createElement("div");
3220
+ backdrop.className = "offline-redemption-backdrop";
3221
+ backdrop.addEventListener("click", () => this.close());
3222
+ // Modal content
3223
+ const modal = document.createElement("div");
3224
+ modal.className = "offline-redemption-modal";
3225
+ // Close button
3226
+ const closeBtn = document.createElement("button");
3227
+ closeBtn.className = "offline-redemption-close-btn";
3228
+ closeBtn.setAttribute("aria-label", "Close modal");
3229
+ 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>`;
3230
+ closeBtn.addEventListener("click", () => this.close());
3231
+ // Header
3232
+ const header = document.createElement("div");
3233
+ header.className = "offline-redemption-header";
3234
+ const title = document.createElement("h2");
3235
+ title.className = "offline-redemption-title";
3236
+ title.textContent = "Redeem In-Store";
3237
+ header.appendChild(title);
3238
+ header.appendChild(closeBtn);
3239
+ // Body
3240
+ const body = document.createElement("div");
3241
+ body.className = "offline-redemption-body";
3242
+ // QR Code section
3243
+ const qrSection = document.createElement("div");
3244
+ qrSection.className = "offline-redemption-qr-section";
3245
+ const qrImage = document.createElement("img");
3246
+ qrImage.className = "offline-redemption-qr-image";
3247
+ qrImage.alt = "Redemption QR Code";
3248
+ qrImage.src = buildQRUrl(this.offlineRedemption.qr_code_data);
3249
+ // QR image fallback handler
3250
+ qrImage.addEventListener("error", () => {
3251
+ qrImage.style.display = "none";
3252
+ const errorMsg = document.createElement("div");
3253
+ errorMsg.className = "offline-redemption-qr-error";
3254
+ errorMsg.textContent = "QR code unavailable";
3255
+ qrSection.appendChild(errorMsg);
3256
+ });
3257
+ qrSection.appendChild(qrImage);
3258
+ // Redemption code section
3259
+ const codeSection = document.createElement("div");
3260
+ codeSection.className = "offline-redemption-code-section";
3261
+ const codeLabel = document.createElement("p");
3262
+ codeLabel.className = "offline-redemption-code-label";
3263
+ codeLabel.textContent = "Redemption Code:";
3264
+ const codeValue = document.createElement("div");
3265
+ codeValue.className = "offline-redemption-code-value";
3266
+ codeValue.textContent = this.offlineRedemption.redemption_code;
3267
+ codeSection.appendChild(codeLabel);
3268
+ codeSection.appendChild(codeValue);
3269
+ // Expiry section
3270
+ const expirySection = document.createElement("div");
3271
+ expirySection.className = "offline-redemption-expiry-section";
3272
+ const expiryLabel = document.createElement("p");
3273
+ expiryLabel.className = "offline-redemption-expiry-label";
3274
+ expiryLabel.textContent = "Valid Until:";
3275
+ const expiryValue = document.createElement("p");
3276
+ expiryValue.className = "offline-redemption-expiry-value";
3277
+ const isValid = isRedemptionValid(this.offlineRedemption.offline_expires_at);
3278
+ expiryValue.textContent = formatExpiryDate(this.offlineRedemption.offline_expires_at);
3279
+ if (!isValid) {
3280
+ expiryValue.classList.add("expired");
3281
+ }
3282
+ expirySection.appendChild(expiryLabel);
3283
+ expirySection.appendChild(expiryValue);
3284
+ body.appendChild(qrSection);
3285
+ body.appendChild(codeSection);
3286
+ body.appendChild(expirySection);
3287
+ // Actions
3288
+ const actions = document.createElement("div");
3289
+ actions.className = "offline-redemption-actions";
3290
+ const downloadBtn = document.createElement("button");
3291
+ downloadBtn.className = "offline-redemption-action-btn download-btn";
3292
+ downloadBtn.textContent = "Download QR";
3293
+ downloadBtn.addEventListener("click", () => this.handleDownloadQR(downloadBtn));
3294
+ const copyBtn = document.createElement("button");
3295
+ copyBtn.className = "offline-redemption-action-btn copy-btn";
3296
+ copyBtn.textContent = "Copy Code";
3297
+ copyBtn.addEventListener("click", () => this.handleCopyCode(copyBtn));
3298
+ actions.appendChild(downloadBtn);
3299
+ actions.appendChild(copyBtn);
3300
+ // Assemble modal
3301
+ modal.appendChild(header);
3302
+ modal.appendChild(body);
3303
+ modal.appendChild(actions);
3304
+ container.appendChild(backdrop);
3305
+ container.appendChild(modal);
3306
+ // Inject styles
3307
+ this.injectStyles();
3308
+ return container;
3309
+ }
3310
+ /**
3311
+ * Handle download QR button click
3312
+ */
3313
+ async handleDownloadQR(button) {
3314
+ const originalText = button.textContent;
3315
+ button.disabled = true;
3316
+ button.textContent = "Downloading...";
3317
+ const success = await downloadQRCode(buildQRUrl(this.offlineRedemption.qr_code_data), "redemption-qr.png");
3318
+ button.textContent = success ? "Downloaded!" : "Download Failed";
3319
+ setTimeout(() => {
3320
+ button.textContent = originalText;
3321
+ button.disabled = false;
3322
+ }, 2000);
3323
+ }
3324
+ /**
3325
+ * Handle copy code button click
3326
+ */
3327
+ async handleCopyCode(button) {
3328
+ const originalText = button.textContent;
3329
+ button.disabled = true;
3330
+ const success = await copyRedemptionCode(this.offlineRedemption.redemption_code);
3331
+ button.textContent = success ? "Copied!" : "Copy Failed";
3332
+ setTimeout(() => {
3333
+ button.textContent = originalText;
3334
+ button.disabled = false;
3335
+ }, 2000);
3336
+ }
3337
+ /**
3338
+ * Inject modal styles
3339
+ */
3340
+ injectStyles() {
3341
+ // Check if styles already injected
3342
+ if (document.getElementById("offline-redemption-modal-styles")) {
3343
+ return;
3344
+ }
3345
+ const style = document.createElement("style");
3346
+ style.id = "offline-redemption-modal-styles";
3347
+ style.textContent = `
3348
+ .offline-redemption-modal-overlay {
3349
+ position: fixed;
3350
+ top: 0;
3351
+ left: 0;
3352
+ right: 0;
3353
+ bottom: 0;
3354
+ display: flex;
3355
+ align-items: center;
3356
+ justify-content: center;
3357
+ z-index: 10000;
3358
+ opacity: 0;
3359
+ transition: opacity 0.3s ease-out;
3360
+ }
3361
+
3362
+ .offline-redemption-modal-overlay.offline-redemption-modal-open {
3363
+ opacity: 1;
3364
+ }
3365
+
3366
+ .offline-redemption-backdrop {
3367
+ position: absolute;
3368
+ top: 0;
3369
+ left: 0;
3370
+ right: 0;
3371
+ bottom: 0;
3372
+ background: rgba(0, 0, 0, 0.5);
3373
+ cursor: pointer;
3374
+ }
3375
+
3376
+ .offline-redemption-modal {
3377
+ position: relative;
3378
+ z-index: 10001;
3379
+ background: white;
3380
+ border-radius: 12px;
3381
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
3382
+ max-width: 400px;
3383
+ width: 90%;
3384
+ max-height: 90vh;
3385
+ overflow-y: auto;
3386
+ animation: slideUp 0.3s ease-out;
3387
+ }
3388
+
3389
+ @keyframes slideUp {
3390
+ from {
3391
+ opacity: 0;
3392
+ transform: translateY(40px);
3393
+ }
3394
+ to {
3395
+ opacity: 1;
3396
+ transform: translateY(0);
3397
+ }
3398
+ }
3399
+
3400
+ .offline-redemption-header {
3401
+ display: flex;
3402
+ justify-content: space-between;
3403
+ align-items: center;
3404
+ padding: 20px;
3405
+ border-bottom: 1px solid #f0f0f0;
3406
+ }
3407
+
3408
+ .offline-redemption-title {
3409
+ margin: 0;
3410
+ font-size: 20px;
3411
+ font-weight: 600;
3412
+ color: #111827;
3413
+ }
3414
+
3415
+ .offline-redemption-close-btn {
3416
+ background: none;
3417
+ border: none;
3418
+ padding: 0;
3419
+ cursor: pointer;
3420
+ display: flex;
3421
+ align-items: center;
3422
+ justify-content: center;
3423
+ color: #6b7280;
3424
+ transition: color 0.2s;
3425
+ }
3426
+
3427
+ .offline-redemption-close-btn:hover {
3428
+ color: #111827;
3429
+ }
3430
+
3431
+ .offline-redemption-close-btn svg {
3432
+ width: 24px;
3433
+ height: 24px;
3434
+ }
3435
+
3436
+ .offline-redemption-body {
3437
+ padding: 24px 20px;
3438
+ display: flex;
3439
+ flex-direction: column;
3440
+ gap: 20px;
3441
+ }
3442
+
3443
+ .offline-redemption-qr-section {
3444
+ display: flex;
3445
+ justify-content: center;
3446
+ background: #f9fafb;
3447
+ padding: 16px;
3448
+ border-radius: 8px;
3449
+ min-height: 200px;
3450
+ align-items: center;
3451
+ }
3452
+
3453
+ .offline-redemption-qr-image {
3454
+ max-width: 100%;
3455
+ max-height: 200px;
3456
+ width: auto;
3457
+ height: auto;
3458
+ }
3459
+
3460
+ .offline-redemption-qr-error {
3461
+ text-align: center;
3462
+ color: #6b7280;
3463
+ font-size: 14px;
3464
+ }
3465
+
3466
+ .offline-redemption-code-section,
3467
+ .offline-redemption-expiry-section {
3468
+ display: flex;
3469
+ flex-direction: column;
3470
+ gap: 8px;
3471
+ }
3472
+
3473
+ .offline-redemption-code-label,
3474
+ .offline-redemption-expiry-label {
3475
+ margin: 0;
3476
+ font-size: 12px;
3477
+ font-weight: 600;
3478
+ color: #6b7280;
3479
+ text-transform: uppercase;
3480
+ letter-spacing: 0.5px;
3481
+ }
3482
+
3483
+ .offline-redemption-code-value {
3484
+ font-family: monospace;
3485
+ font-size: 16px;
3486
+ font-weight: 600;
3487
+ color: #111827;
3488
+ padding: 12px;
3489
+ background: #f9fafb;
3490
+ border: 1px solid #e5e7eb;
3491
+ border-radius: 6px;
3492
+ text-align: center;
3493
+ letter-spacing: 2px;
3494
+ }
3495
+
3496
+ .offline-redemption-expiry-value {
3497
+ margin: 0;
3498
+ font-size: 14px;
3499
+ color: #374151;
3500
+ }
3501
+
3502
+ .offline-redemption-expiry-value.expired {
3503
+ color: #dc2626;
3504
+ font-weight: 600;
3505
+ }
3506
+
3507
+ .offline-redemption-actions {
3508
+ display: flex;
3509
+ gap: 12px;
3510
+ padding: 20px;
3511
+ border-top: 1px solid #f0f0f0;
3512
+ }
3513
+
3514
+ .offline-redemption-action-btn {
3515
+ flex: 1;
3516
+ padding: 12px 16px;
3517
+ border: none;
3518
+ border-radius: 6px;
3519
+ font-size: 14px;
3520
+ font-weight: 600;
3521
+ cursor: pointer;
3522
+ transition: all 0.2s;
3523
+ }
3524
+
3525
+ .download-btn {
3526
+ background: #3b82f6;
3527
+ color: white;
3528
+ }
3529
+
3530
+ .download-btn:hover:not(:disabled) {
3531
+ background: #2563eb;
3532
+ }
3533
+
3534
+ .copy-btn {
3535
+ background: #f3f4f6;
3536
+ color: #111827;
3537
+ border: 1px solid #e5e7eb;
3538
+ }
3539
+
3540
+ .copy-btn:hover:not(:disabled) {
3541
+ background: #e5e7eb;
3542
+ }
3543
+
3544
+ .offline-redemption-action-btn:disabled {
3545
+ opacity: 0.7;
3546
+ cursor: not-allowed;
3547
+ }
3548
+
3549
+ /* Mobile responsiveness */
3550
+ @media (max-width: 480px) {
3551
+ .offline-redemption-modal {
3552
+ width: 95%;
3553
+ max-height: 95vh;
3554
+ }
3555
+
3556
+ .offline-redemption-body {
3557
+ padding: 16px;
3558
+ }
3559
+
3560
+ .offline-redemption-actions {
3561
+ padding: 16px;
3562
+ }
3563
+ }
3564
+ `;
3565
+ document.head.appendChild(style);
3566
+ }
3567
+ }
3568
+
2652
3569
  /// <reference lib="dom" />
2653
3570
  /**
2654
3571
  * Widget state enumeration for tracking render lifecycle
@@ -2683,6 +3600,8 @@ class WidgetRoot {
2683
3600
  this.lastGroupDataRefreshTime = 0;
2684
3601
  this.GROUP_REFRESH_DEBOUNCE = 1000; // 1 second min between refreshes
2685
3602
  this.groupExpiryRefreshTriggered = false; // Track if expiry refresh already triggered for current group
3603
+ this.offlineRedemption = null;
3604
+ this.offlineRedemptionModal = null;
2686
3605
  /** Handle backend fulfillment notifications */
2687
3606
  this.handleGroupFulfilledEvent = (event) => {
2688
3607
  const detail = event.detail;
@@ -2839,11 +3758,22 @@ class WidgetRoot {
2839
3758
  }
2840
3759
  /** Persist fulfilled state, emit callback, and refresh UI */
2841
3760
  processGroupFulfilled(eventData) {
2842
- var _a, _b;
3761
+ var _a, _b, _c, _d;
2843
3762
  this.groupFulfilled = true;
2844
- this.frozenReward = eventData.reward || null;
2845
- this.currentGroupId = eventData.groupId;
2846
- (_b = (_a = this.events) === null || _a === void 0 ? void 0 : _a.onGroupFulfilled) === null || _b === void 0 ? void 0 : _b.call(_a, eventData);
3763
+ // Extract reward from new event structure or fallback to legacy
3764
+ const reward = ((_a = eventData.frozen_reward) === null || _a === void 0 ? void 0 : _a.reward) || eventData.reward || null;
3765
+ this.frozenReward = reward;
3766
+ // Extract group ID from new event structure or fallback to legacy
3767
+ this.currentGroupId = ((_b = eventData.group) === null || _b === void 0 ? void 0 : _b.id) || eventData.groupId || null;
3768
+ // Store offline redemption data if available
3769
+ if (eventData.offline_redemption && isValidOfflineRedemption(eventData.offline_redemption)) {
3770
+ this.offlineRedemption = eventData.offline_redemption;
3771
+ this.logger.info("Offline redemption data stored", {
3772
+ member_id: this.offlineRedemption.member_id,
3773
+ code: this.offlineRedemption.redemption_code,
3774
+ });
3775
+ }
3776
+ (_d = (_c = this.events) === null || _c === void 0 ? void 0 : _c.onGroupFulfilled) === null || _d === void 0 ? void 0 : _d.call(_c, eventData);
2847
3777
  this.renderFulfilledState();
2848
3778
  }
2849
3779
  /** Re-render widget and external containers to reflect fulfillment */
@@ -2958,16 +3888,42 @@ class WidgetRoot {
2958
3888
  rewardCheck.textContent = "✔";
2959
3889
  rewardCheck.style.color = "var(--cobuy-success-color, #10b981)";
2960
3890
  const rewardLabel = document.createElement("span");
2961
- rewardLabel.textContent = rewardText ? `Reward available: ${rewardText}` : "Reward available";
3891
+ rewardLabel.textContent = rewardText
3892
+ ? `Discount available: ${rewardText}`
3893
+ : "Discount available";
2962
3894
  rewardLine.appendChild(rewardCheck);
2963
3895
  rewardLine.appendChild(rewardLabel);
2964
3896
  root.appendChild(title);
2965
3897
  root.appendChild(rewardLine);
2966
- // Create footer with View all Groups link at bottom right
3898
+ // Create footer with action CTAs
2967
3899
  const footer = document.createElement("div");
2968
3900
  footer.style.display = "flex";
2969
3901
  footer.style.justifyContent = "flex-end";
2970
3902
  footer.style.alignItems = "center";
3903
+ footer.style.gap = "12px";
3904
+ // Add "Redeem In-store" CTA if offline redemption is available
3905
+ if (this.offlineRedemption && isValidOfflineRedemption(this.offlineRedemption)) {
3906
+ const redeemLink = document.createElement("button");
3907
+ redeemLink.className = "cobuy-redeem-instore-link";
3908
+ redeemLink.style.background = "none";
3909
+ redeemLink.style.border = "none";
3910
+ redeemLink.style.color = "var(--cobuy-primary-color, #3b82f6)";
3911
+ redeemLink.style.cursor = "pointer";
3912
+ redeemLink.style.fontSize = "12px";
3913
+ redeemLink.style.fontWeight = "600";
3914
+ redeemLink.style.textDecoration = "underline";
3915
+ redeemLink.style.padding = "0";
3916
+ redeemLink.style.transition = "opacity 0.2s";
3917
+ redeemLink.textContent = "Redeem In-store";
3918
+ redeemLink.addEventListener("click", () => this.openOfflineRedemptionModal());
3919
+ redeemLink.addEventListener("mouseover", () => {
3920
+ redeemLink.style.opacity = "0.7";
3921
+ });
3922
+ redeemLink.addEventListener("mouseout", () => {
3923
+ redeemLink.style.opacity = "1";
3924
+ });
3925
+ footer.appendChild(redeemLink);
3926
+ }
2971
3927
  root.appendChild(footer);
2972
3928
  return root;
2973
3929
  }
@@ -3151,6 +4107,12 @@ class WidgetRoot {
3151
4107
  this.currentGroupData = groupData;
3152
4108
  this.currentGroupId = (groupData === null || groupData === void 0 ? void 0 : groupData.id) || null;
3153
4109
  this.currentRewardData = rewardData;
4110
+ 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));
4111
+ // Extract offline_redemption from API response if available
4112
+ if ((groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption) &&
4113
+ isValidOfflineRedemption(groupData.offline_redemption)) {
4114
+ this.offlineRedemption = groupData.offline_redemption;
4115
+ }
3154
4116
  // Check if group is already fulfilled (full) on initial load
3155
4117
  if (groupData) {
3156
4118
  const participants = Number(groupData.participants_count || 0);
@@ -3569,16 +4531,14 @@ class WidgetRoot {
3569
4531
  // rewardText ? `Reward locked in: ${rewardText}` : "Reward available for this group",
3570
4532
  // );
3571
4533
  // rewardLine.title = rewardLine.textContent;
3572
- // }
3573
- // else if (activeReward) {
4534
+ // } else if (activeReward) {
3574
4535
  // const rewardText = this.formatRewardText(activeReward);
3575
4536
  // rewardLine.textContent = rewardText
3576
4537
  // ? `Save up to ${rewardText} with CoBuy`
3577
4538
  // : "CoBuy reward available";
3578
4539
  // rewardLine.setAttribute("aria-label", `Eligible for CoBuy reward: ${rewardLine.textContent}`);
3579
4540
  // rewardLine.title = rewardLine.textContent;
3580
- // }
3581
- // else {
4541
+ // } else {
3582
4542
  // rewardLine.textContent = "CoBuy offer loading or unavailable";
3583
4543
  // rewardLine.setAttribute("aria-label", "CoBuy offer loading or unavailable");
3584
4544
  // rewardLine.title = "CoBuy offer loading or unavailable";
@@ -3814,6 +4774,12 @@ class WidgetRoot {
3814
4774
  }
3815
4775
  groupJoinData = joinResponse.data;
3816
4776
  this.logger.info("Successfully joined group", groupJoinData);
4777
+ this.offlineRedemption =
4778
+ groupJoinData &&
4779
+ groupJoinData.offline_redemption &&
4780
+ isValidOfflineRedemption(groupJoinData.offline_redemption)
4781
+ ? groupJoinData.offline_redemption
4782
+ : null;
3817
4783
  // Trigger invite tracking before opening lobby (global for product)
3818
4784
  try {
3819
4785
  const inviteResponse = await this.apiClient.inviteToGroup(this.currentGroupId, "copy_link");
@@ -3859,6 +4825,13 @@ class WidgetRoot {
3859
4825
  const isGroupFulfilled = groupJoinData.group.participants_count >= groupJoinData.group.max_participants;
3860
4826
  const groupLinkFromInvite = inviteData === null || inviteData === void 0 ? void 0 : inviteData.invite_url;
3861
4827
  const shareMessageFromInvite = inviteData === null || inviteData === void 0 ? void 0 : inviteData.share_message;
4828
+ console.log("offlien dataaa", groupJoinData.offline_redemption, isValidOfflineRedemption(groupJoinData.offline_redemption));
4829
+ // Extract offline_redemption from join response if available
4830
+ const offlineRedemptionFromJoin = groupJoinData.offline_redemption &&
4831
+ isValidOfflineRedemption(groupJoinData.offline_redemption)
4832
+ ? groupJoinData.offline_redemption
4833
+ : undefined;
4834
+ this.offlineRedemption = offlineRedemptionFromJoin || null;
3862
4835
  // Prepare modal data with real values from join response
3863
4836
  cobuySDK.openModal({
3864
4837
  productId,
@@ -3877,6 +4850,7 @@ class WidgetRoot {
3877
4850
  groupLink: groupLinkFromInvite || `https://cobuy.group/lobby/${groupJoinData.group.id}`,
3878
4851
  shareMessage: shareMessageFromInvite,
3879
4852
  isLocked: !isGroupFulfilled,
4853
+ offlineRedemption: offlineRedemptionFromJoin,
3880
4854
  activities: [
3881
4855
  {
3882
4856
  emoji: "👤",
@@ -3926,6 +4900,20 @@ class WidgetRoot {
3926
4900
  }
3927
4901
  }
3928
4902
  }
4903
+ /**
4904
+ * Open offline redemption modal when user clicks "Redeem In-store"
4905
+ */
4906
+ openOfflineRedemptionModal() {
4907
+ if (!this.offlineRedemption || !isValidOfflineRedemption(this.offlineRedemption)) {
4908
+ this.logger.warn("Offline redemption data not available");
4909
+ return;
4910
+ }
4911
+ this.logger.info("Opening offline redemption modal");
4912
+ this.offlineRedemptionModal = new OfflineRedemptionModal(this.offlineRedemption, () => {
4913
+ this.offlineRedemptionModal = null;
4914
+ });
4915
+ this.offlineRedemptionModal.open();
4916
+ }
3929
4917
  /**
3930
4918
  * Emit checkout intent without performing navigation
3931
4919
  */
@@ -3984,13 +4972,13 @@ class WidgetRoot {
3984
4972
  const reward = this.frozenReward || (rewardData === null || rewardData === void 0 ? void 0 : rewardData.reward);
3985
4973
  if (this.groupFulfilled) {
3986
4974
  const rewardText = this.formatRewardText(reward);
3987
- return rewardText ? `Reward available: ${rewardText}` : "Reward available for this group";
4975
+ return rewardText ? `Discount available: ${rewardText}` : "Discount available for this group";
3988
4976
  }
3989
4977
  if (!reward) {
3990
- return "Join with CoBuy to unlock rewards";
4978
+ return "Join with CoBuy to unlock discount";
3991
4979
  }
3992
4980
  const rewardText = this.formatRewardText(reward);
3993
- return rewardText ? `Save up to ${rewardText} with CoBuy` : "CoBuy reward available";
4981
+ return rewardText ? `Save up to ${rewardText} with CoBuy` : "CoBuy discount available";
3994
4982
  }
3995
4983
  /**
3996
4984
  * Public hook to request a realtime refresh from external callers
@@ -4549,12 +5537,28 @@ class ApiClient {
4549
5537
  * @param checkoutRef - The checkout reference ID from the prepare step
4550
5538
  * @returns Promise with success status
4551
5539
  */
4552
- async confirmCheckout(groupId, checkoutRef) {
5540
+ async confirmCheckout(groupId, checkoutRef, data) {
4553
5541
  const endpoint = buildApiUrl("", API_ENDPOINTS.GROUP_CHECKOUT_CONFIRM, { groupId });
4554
5542
  this.logger.info(`Confirming checkout for group: ${groupId}`);
4555
- const response = await this.post(endpoint, {
5543
+ const payload = {
4556
5544
  checkout_ref: checkoutRef,
4557
- });
5545
+ };
5546
+ // Add optional fields if provided
5547
+ if (data) {
5548
+ if (data.order_id)
5549
+ payload.order_id = data.order_id;
5550
+ if (data.order_total !== undefined)
5551
+ payload.order_total = data.order_total;
5552
+ if (data.discount_applied !== undefined)
5553
+ payload.discount_applied = data.discount_applied;
5554
+ if (data.currency)
5555
+ payload.currency = data.currency;
5556
+ if (data.payment_method)
5557
+ payload.payment_method = data.payment_method;
5558
+ if (data.metadata)
5559
+ payload.metadata = data.metadata;
5560
+ }
5561
+ const response = await this.post(endpoint, payload);
4558
5562
  if (response.success) {
4559
5563
  this.logger.info("Checkout confirmed successfully");
4560
5564
  return {
@@ -4588,7 +5592,7 @@ class AnalyticsClient {
4588
5592
  */
4589
5593
  async trackCtaClick(productId) {
4590
5594
  const event = {
4591
- event: "cta_clicked",
5595
+ event: "CTA_CLICKED",
4592
5596
  productId,
4593
5597
  timestamp: new Date().toISOString(),
4594
5598
  sessionId: this.sessionId,
@@ -4632,6 +5636,28 @@ class AnalyticsClient {
4632
5636
  throw error;
4633
5637
  }
4634
5638
  }
5639
+ /**
5640
+ * Track page view event
5641
+ */
5642
+ async trackPageView() {
5643
+ const event = {
5644
+ event: "PAGE_VIEW",
5645
+ timestamp: new Date().toISOString(),
5646
+ context: {
5647
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
5648
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
5649
+ sdkVersion: this.sdkVersion,
5650
+ },
5651
+ };
5652
+ try {
5653
+ await this.sendEvent(event);
5654
+ this.logger.info("[Analytics] Page view tracked");
5655
+ }
5656
+ catch (error) {
5657
+ // Log but don't throw - analytics failure should not break UX
5658
+ this.logger.error("[Analytics] Failed to track page view", error);
5659
+ }
5660
+ }
4635
5661
  /**
4636
5662
  * Track custom event (extensible for future use)
4637
5663
  */
@@ -8819,9 +9845,10 @@ Object.assign(lookup, {
8819
9845
  * Manages a single socket.io connection for the SDK
8820
9846
  */
8821
9847
  class SocketManager {
8822
- constructor(config, sdkVersion) {
9848
+ constructor(config, sdkVersion, sessionId) {
8823
9849
  this.config = config;
8824
9850
  this.sdkVersion = sdkVersion;
9851
+ this.sessionId = sessionId;
8825
9852
  this.socket = null;
8826
9853
  this.logger = new Logger(config.debug);
8827
9854
  }
@@ -8866,6 +9893,7 @@ class SocketManager {
8866
9893
  auth: {
8867
9894
  merchantKey: this.config.merchantKey,
8868
9895
  sdkVersion: this.sdkVersion,
9896
+ sessionId: this.sessionId,
8869
9897
  },
8870
9898
  autoConnect: true,
8871
9899
  });
@@ -9052,7 +10080,9 @@ class CoBuy {
9052
10080
  const ref = window.localStorage.getItem(key);
9053
10081
  if (ref) {
9054
10082
  this.logger.debug(`[SDK] Retrieved checkout reference via prefix: ${key}`);
9055
- const parsedGroupId = key.startsWith(`${basePrefix}_`) ? key.substring(basePrefix.length + 1) : null;
10083
+ const parsedGroupId = key.startsWith(`${basePrefix}_`)
10084
+ ? key.substring(basePrefix.length + 1)
10085
+ : null;
9056
10086
  return { key, checkoutRef: ref, groupId: parsedGroupId };
9057
10087
  }
9058
10088
  }
@@ -9142,7 +10172,7 @@ class CoBuy {
9142
10172
  // Initialize real-time socket connection for public mode
9143
10173
  if (config.authMode === "public" && config.merchantKey) {
9144
10174
  try {
9145
- this.socketManager = new SocketManager(config, SDK_VERSION);
10175
+ this.socketManager = new SocketManager(config, SDK_VERSION, this.sessionId);
9146
10176
  this.socketManager.connect();
9147
10177
  this.socketManager.bindHandlers({
9148
10178
  onGroupFulfilled: (payload) => {
@@ -9184,6 +10214,13 @@ class CoBuy {
9184
10214
  this.logger.warn("[SDK] Failed to initialize sockets", e);
9185
10215
  }
9186
10216
  }
10217
+ // Track page view event after successful initialization
10218
+ if (this.analyticsClient) {
10219
+ this.analyticsClient.trackPageView().catch((error) => {
10220
+ // Non-blocking: Analytics failure should not affect SDK initialization
10221
+ this.logger.warn("[SDK] Failed to track page view", error);
10222
+ });
10223
+ }
9187
10224
  this.logger.info("SDK initialization complete");
9188
10225
  }
9189
10226
  catch (error) {
@@ -9263,6 +10300,7 @@ class CoBuy {
9263
10300
  groupLink: options.groupLink,
9264
10301
  activities: options.activities,
9265
10302
  isLocked: options.isLocked,
10303
+ offlineRedemption: options.offlineRedemption,
9266
10304
  };
9267
10305
  // Create modal instance
9268
10306
  const modal = new LobbyModal(modalData, {
@@ -9424,14 +10462,31 @@ class CoBuy {
9424
10462
  *
9425
10463
  * @param groupId - The group ID to confirm checkout for
9426
10464
  * @param checkoutRef - The checkout reference ID from the prepare step
10465
+ /**
10466
+ * Confirm checkout for a group
10467
+ * Should be called after validateCheckout to complete the checkout process.
10468
+ *
10469
+ * @param groupId The group ID to confirm checkout for
10470
+ * @param checkoutRef The checkout reference from the prepare step
10471
+ * @param data Optional order details (order_id, order_total, discount_applied, currency, payment_method, metadata)
9427
10472
  *
9428
10473
  * @example
9429
10474
  * ```typescript
9430
10475
  * // Confirm checkout for a group
9431
10476
  * await CoBuy.confirmCheckout('fae238ae-7468-47e9-9eec-b6d52fe3b012', 'chk_9a6d8750-ed60-4795-a207-2abe955e8509');
10477
+ *
10478
+ * // Confirm checkout with order details
10479
+ * await CoBuy.confirmCheckout('fae238ae-7468-47e9-9eec-b6d52fe3b012', 'chk_9a6d8750-ed60-4795-a207-2abe955e8509', {
10480
+ * order_id: 'ORDER-1001',
10481
+ * order_total: 100.00,
10482
+ * discount_applied: 20.00,
10483
+ * currency: 'USD',
10484
+ * payment_method: 'card',
10485
+ * metadata: { coupon_code: 'WELCOME20', customer_id: 'cust_123' }
10486
+ * });
9432
10487
  * ```
9433
10488
  */
9434
- async confirmCheckout(groupId, checkoutRef) {
10489
+ async confirmCheckout(groupId, checkoutRef, data) {
9435
10490
  if (!this.configManager.isInitialized()) {
9436
10491
  this.logger.warn("SDK not initialized, cannot confirm checkout");
9437
10492
  return;
@@ -9441,7 +10496,7 @@ class CoBuy {
9441
10496
  return;
9442
10497
  }
9443
10498
  try {
9444
- const response = await this.apiClient.confirmCheckout(groupId, checkoutRef);
10499
+ const response = await this.apiClient.confirmCheckout(groupId, checkoutRef, data);
9445
10500
  if (response.success) {
9446
10501
  this.logger.info(`Checkout confirmed successfully for group: ${groupId}`);
9447
10502
  // Directly refresh widgets. If we can infer productId, refresh only matching widgets.