@cshah18/sdk 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  */
@@ -1632,9 +1852,12 @@ class LobbyModal {
1632
1852
  // Top section
1633
1853
  const topSection = this.createTopSection();
1634
1854
  mainContent.appendChild(topSection);
1635
- // Activity section
1636
- const activitySection = this.createActivitySection();
1637
- mainContent.appendChild(activitySection);
1855
+ // Activity section (hidden when offline redemption is present to keep layout compact)
1856
+ const hasOfflineRedemption = this.data.offlineRedemption && isValidOfflineRedemption(this.data.offlineRedemption);
1857
+ if (!hasOfflineRedemption) {
1858
+ const activitySection = this.createActivitySection();
1859
+ mainContent.appendChild(activitySection);
1860
+ }
1638
1861
  mainWrapper.appendChild(mainContent);
1639
1862
  modal.appendChild(background);
1640
1863
  modal.appendChild(mainWrapper);
@@ -1677,9 +1900,9 @@ class LobbyModal {
1677
1900
  // Title
1678
1901
  const titleWp = this.createTitleSection();
1679
1902
  leftWp.appendChild(titleWp);
1680
- // Link
1681
- const linkWp = this.createLinkSection();
1682
- leftWp.appendChild(linkWp);
1903
+ // Connected section (subtitle + link + share)
1904
+ const connectedSection = this.createConnectedSection();
1905
+ leftWp.appendChild(connectedSection);
1683
1906
  // Offer
1684
1907
  const offerBox = this.createOfferSection();
1685
1908
  leftWp.appendChild(offerBox);
@@ -1691,24 +1914,35 @@ class LobbyModal {
1691
1914
  createStatusSection() {
1692
1915
  var _a;
1693
1916
  const statusWp = document.createElement("div");
1694
- statusWp.className = "lobby-status-wp";
1917
+ statusWp.className = "lobby-status-section";
1695
1918
  // Use safe defaults so UI never shows undefined values
1696
1919
  const groupNum = (_a = this.data.groupNumber) !== null && _a !== void 0 ? _a : "1000";
1697
1920
  const status = this.data.status === "complete" ? "complete" : "active";
1698
- const statusLabel = document.createElement("div");
1699
- statusLabel.className = "lobby-status";
1700
- statusLabel.textContent = "Status";
1921
+ // New "You are in the lobby" indicator badge
1922
+ const lobbyIndicator = document.createElement("div");
1923
+ lobbyIndicator.className = "lobby-indicator";
1924
+ const pulsingDot = document.createElement("div");
1925
+ pulsingDot.className = "pulsing-dot";
1926
+ const indicatorText = document.createElement("span");
1927
+ indicatorText.className = "indicator-text";
1928
+ indicatorText.textContent = "YOU ARE IN THE LOBBY";
1929
+ lobbyIndicator.appendChild(pulsingDot);
1930
+ lobbyIndicator.appendChild(indicatorText);
1931
+ // Status row with badge and group number
1932
+ const statusRow = document.createElement("div");
1933
+ statusRow.className = "lobby-status-wp";
1701
1934
  // Render only the current status chip
1702
1935
  const statusChip = document.createElement("div");
1703
1936
  statusChip.className = `lobby-status ${status}`;
1704
1937
  statusChip.id = "lobbyStatusChip";
1705
- statusChip.textContent = status === "complete" ? "Complete" : "Active";
1938
+ statusChip.textContent = status === "complete" ? "COMPLETED" : "ACTIVE";
1706
1939
  const groupNumber = document.createElement("div");
1707
1940
  groupNumber.className = "lobby-number";
1708
1941
  groupNumber.textContent = `Group #${groupNum}`;
1709
- statusWp.appendChild(statusLabel);
1710
- statusWp.appendChild(statusChip);
1711
- statusWp.appendChild(groupNumber);
1942
+ statusRow.appendChild(statusChip);
1943
+ statusRow.appendChild(groupNumber);
1944
+ statusWp.appendChild(lobbyIndicator);
1945
+ statusWp.appendChild(statusRow);
1712
1946
  return statusWp;
1713
1947
  }
1714
1948
  /**
@@ -1721,24 +1955,49 @@ class LobbyModal {
1721
1955
  const title = document.createElement("h2");
1722
1956
  title.id = "lobbyTitleText";
1723
1957
  title.textContent = titleContent.title;
1958
+ titleWp.appendChild(title);
1959
+ return titleWp;
1960
+ }
1961
+ /**
1962
+ * Create connected section (subtitle + link + share)
1963
+ */
1964
+ createConnectedSection() {
1965
+ const connectedSection = document.createElement("div");
1966
+ connectedSection.className = "connected-section";
1967
+ connectedSection.id = "lobbyConnectedSection";
1968
+ const titleContent = this.getTitleText(this.data);
1969
+ // Subtitle
1724
1970
  const subtitle = document.createElement("p");
1725
1971
  subtitle.className = "sub-title";
1726
1972
  subtitle.id = "lobbySubtitleText";
1727
1973
  subtitle.textContent = titleContent.subtitle;
1728
- titleWp.appendChild(title);
1729
- titleWp.appendChild(subtitle);
1730
- return titleWp;
1974
+ connectedSection.appendChild(subtitle);
1975
+ // Check if group is fulfilled and has offline redemption
1976
+ const isComplete = !this.computeIsLocked(this.data);
1977
+ const hasOfflineRedemption = isComplete &&
1978
+ this.data.offlineRedemption &&
1979
+ isValidOfflineRedemption(this.data.offlineRedemption);
1980
+ if (hasOfflineRedemption) {
1981
+ // Show offline redemption view with integrated actions
1982
+ const offlineSection = this.createOfflineRedemptionSection(this.data.offlineRedemption);
1983
+ connectedSection.appendChild(offlineSection);
1984
+ }
1985
+ else {
1986
+ // Show link and share container
1987
+ const linkShareContainer = document.createElement("div");
1988
+ linkShareContainer.className = "link-share-container";
1989
+ const linkWp = this.createLinkSection();
1990
+ linkShareContainer.appendChild(linkWp);
1991
+ connectedSection.appendChild(linkShareContainer);
1992
+ }
1993
+ return connectedSection;
1731
1994
  }
1732
1995
  /**
1733
- * Create link section
1996
+ * Create link section (just the link box and share button, no wrapper)
1734
1997
  */
1735
1998
  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
- }
1999
+ const linkShareWrapper = document.createElement("div");
2000
+ linkShareWrapper.className = "link-share-wrapper";
1742
2001
  // Link box
1743
2002
  const linkBox = document.createElement("div");
1744
2003
  linkBox.className = "lobby-link-box";
@@ -1753,23 +2012,324 @@ class LobbyModal {
1753
2012
  linkUrl.textContent = this.data.groupLink || `cobuy.group/lobby/${this.data.productId}`;
1754
2013
  linkBoxInr.appendChild(linkText);
1755
2014
  linkBoxInr.appendChild(linkUrl);
1756
- const copyBtn = document.createElement("div");
2015
+ const copyBtn = document.createElement("button");
1757
2016
  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>`;
2017
+ copyBtn.innerHTML = `<span class="copy-text">Copy</span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
1759
2018
  copyBtn.addEventListener("click", () => this.copyLink());
1760
2019
  linkBox.appendChild(linkBoxInr);
1761
2020
  linkBox.appendChild(copyBtn);
1762
2021
  // Share button
1763
- const shareBtn = document.createElement("div");
1764
- shareBtn.className = "share-btn-wp";
1765
- const shareBtnInner = document.createElement("div");
2022
+ const shareBtnInner = document.createElement("button");
1766
2023
  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>`;
2024
+ shareBtnInner.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-share"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" x2="12" y1="2" y2="15"></line></svg><span class="share-text">Share</span>`;
1768
2025
  shareBtnInner.addEventListener("click", () => this.share());
1769
- shareBtn.appendChild(shareBtnInner);
1770
- linkWp.appendChild(linkBox);
1771
- linkWp.appendChild(shareBtn);
1772
- return linkWp;
2026
+ linkShareWrapper.appendChild(linkBox);
2027
+ linkShareWrapper.appendChild(shareBtnInner);
2028
+ return linkShareWrapper;
2029
+ }
2030
+ /**
2031
+ * Create offline redemption section
2032
+ */
2033
+ createOfflineRedemptionSection(offlineRedemption) {
2034
+ const section = document.createElement("div");
2035
+ section.className = "offline-redemption-section";
2036
+ section.id = "lobbyOfflineRedemptionSection";
2037
+ // Top row: QR + Code side by side
2038
+ const topRow = document.createElement("div");
2039
+ topRow.className = "offline-top-row";
2040
+ // QR Code container
2041
+ const qrContainer = document.createElement("div");
2042
+ qrContainer.className = "offline-qr-container";
2043
+ const qrImage = document.createElement("img");
2044
+ qrImage.className = "offline-qr-image";
2045
+ qrImage.alt = "Redemption QR Code";
2046
+ qrImage.src = buildQRUrl(offlineRedemption.qr_code_data);
2047
+ // Handle QR image load errors
2048
+ qrImage.addEventListener("error", () => {
2049
+ qrImage.style.display = "none";
2050
+ const fallbackText = document.createElement("div");
2051
+ fallbackText.className = "offline-qr-fallback";
2052
+ fallbackText.textContent = "QR Code Unavailable";
2053
+ qrContainer.appendChild(fallbackText);
2054
+ });
2055
+ qrContainer.appendChild(qrImage);
2056
+ // Redemption code box
2057
+ const codeBox = document.createElement("div");
2058
+ codeBox.className = "offline-code-box";
2059
+ const codeLabel = document.createElement("p");
2060
+ codeLabel.className = "offline-code-label";
2061
+ codeLabel.textContent = "Redemption Code";
2062
+ const codeValue = document.createElement("div");
2063
+ codeValue.className = "offline-code-value";
2064
+ codeValue.textContent = offlineRedemption.redemption_code;
2065
+ const copyCodeBtn = document.createElement("button");
2066
+ copyCodeBtn.className = "offline-copy-code-btn";
2067
+ copyCodeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>`;
2068
+ copyCodeBtn.addEventListener("click", () => this.copyOfflineRedemptionCode(offlineRedemption.redemption_code, copyCodeBtn));
2069
+ const codeRow = document.createElement("div");
2070
+ codeRow.className = "offline-code-row";
2071
+ codeRow.appendChild(codeValue);
2072
+ codeRow.appendChild(copyCodeBtn);
2073
+ codeBox.appendChild(codeLabel);
2074
+ codeBox.appendChild(codeRow);
2075
+ topRow.appendChild(qrContainer);
2076
+ topRow.appendChild(codeBox);
2077
+ // Expiry info - compact single line
2078
+ const expiryInfo = document.createElement("div");
2079
+ expiryInfo.className = "offline-expiry-info";
2080
+ expiryInfo.innerHTML = `<span class="offline-expiry-label">Valid Until:</span> <span class="offline-expiry-value">${formatExpiryDate(offlineRedemption.offline_expires_at)}</span>`;
2081
+ // Action buttons row
2082
+ const actionsRow = document.createElement("div");
2083
+ actionsRow.className = "offline-actions-row";
2084
+ // Download QR button
2085
+ const downloadQRBtn = document.createElement("button");
2086
+ downloadQRBtn.className = "offline-download-qr-btn";
2087
+ downloadQRBtn.textContent = "Download QR";
2088
+ downloadQRBtn.addEventListener("click", () => this.downloadOfflineQR(offlineRedemption, downloadQRBtn));
2089
+ // Online checkout button
2090
+ const onlineCheckoutBtn = document.createElement("button");
2091
+ onlineCheckoutBtn.className = "offline-online-checkout-btn";
2092
+ onlineCheckoutBtn.textContent = "Checkout Online";
2093
+ onlineCheckoutBtn.addEventListener("click", () => {
2094
+ if (this.data.groupLink) {
2095
+ window.open(this.data.groupLink, "_blank");
2096
+ }
2097
+ });
2098
+ actionsRow.appendChild(downloadQRBtn);
2099
+ actionsRow.appendChild(onlineCheckoutBtn);
2100
+ section.appendChild(topRow);
2101
+ section.appendChild(expiryInfo);
2102
+ section.appendChild(actionsRow);
2103
+ // Inject styles for offline redemption section
2104
+ this.injectOfflineRedemptionStyles();
2105
+ return section;
2106
+ }
2107
+ /**
2108
+ * Handle copying offline redemption code
2109
+ */
2110
+ async copyOfflineRedemptionCode(code, button) {
2111
+ const originalContent = button.innerHTML;
2112
+ const success = await copyRedemptionCode(code);
2113
+ if (success) {
2114
+ button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
2115
+ setTimeout(() => {
2116
+ button.innerHTML = originalContent;
2117
+ }, 2000);
2118
+ }
2119
+ }
2120
+ /**
2121
+ * Handle downloading offline QR code
2122
+ */
2123
+ async downloadOfflineQR(offlineRedemption, button) {
2124
+ const originalText = button.textContent;
2125
+ button.disabled = true;
2126
+ button.textContent = "Downloading...";
2127
+ const success = await downloadQRCode(buildQRUrl(offlineRedemption.qr_code_data), "redemption-qr.png");
2128
+ button.textContent = success ? "Downloaded!" : "Failed";
2129
+ setTimeout(() => {
2130
+ button.textContent = originalText;
2131
+ button.disabled = false;
2132
+ }, 2000);
2133
+ }
2134
+ /**
2135
+ * Inject styles for offline redemption section
2136
+ */
2137
+ injectOfflineRedemptionStyles() {
2138
+ if (document.getElementById("lobby-offline-redemption-styles")) {
2139
+ return;
2140
+ }
2141
+ const style = document.createElement("style");
2142
+ style.id = "lobby-offline-redemption-styles";
2143
+ style.textContent = `
2144
+ .offline-redemption-section {
2145
+ display: flex;
2146
+ flex-direction: column;
2147
+ gap: 14px;
2148
+ padding: 16px;
2149
+ background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
2150
+ border: 1.5px solid #e5e7eb;
2151
+ border-radius: 12px;
2152
+ margin-top: 12px;
2153
+ }
2154
+
2155
+ .offline-top-row {
2156
+ display: flex;
2157
+ gap: 14px;
2158
+ align-items: stretch;
2159
+ }
2160
+
2161
+ .offline-qr-container {
2162
+ flex: 0 0 120px;
2163
+ display: flex;
2164
+ justify-content: center;
2165
+ align-items: center;
2166
+ background: white;
2167
+ padding: 10px;
2168
+ border-radius: 8px;
2169
+ border: 1.5px solid #e5e7eb;
2170
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
2171
+ }
2172
+
2173
+ .offline-qr-image {
2174
+ max-width: 100px;
2175
+ max-height: 100px;
2176
+ width: auto;
2177
+ height: auto;
2178
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.08));
2179
+ }
2180
+
2181
+ .offline-qr-fallback {
2182
+ color: #9ca3af;
2183
+ font-size: 11px;
2184
+ text-align: center;
2185
+ }
2186
+
2187
+ .offline-code-box {
2188
+ flex: 1;
2189
+ display: flex;
2190
+ flex-direction: column;
2191
+ gap: 8px;
2192
+ justify-content: center;
2193
+ }
2194
+
2195
+ .offline-code-row {
2196
+ position: relative;
2197
+ display: flex;
2198
+ align-items: center;
2199
+ }
2200
+
2201
+ .offline-code-label {
2202
+ margin: 0;
2203
+ font-size: 11px;
2204
+ font-weight: 700;
2205
+ color: #1f2937;
2206
+ text-transform: uppercase;
2207
+ letter-spacing: 0.5px;
2208
+ }
2209
+
2210
+ .offline-code-value {
2211
+ font-family: 'Courier New', monospace;
2212
+ font-size: 14px;
2213
+ font-weight: 700;
2214
+ color: #111827;
2215
+ padding: 10px 40px 10px 12px;
2216
+ background: white;
2217
+ border: 1.5px solid #d1d5db;
2218
+ border-radius: 8px;
2219
+ text-align: center;
2220
+ letter-spacing: 1.5px;
2221
+ position: relative;
2222
+ display: flex;
2223
+ align-items: center;
2224
+ justify-content: center;
2225
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
2226
+ transition: all 0.2s;
2227
+ }
2228
+
2229
+ .offline-code-row .offline-code-value {
2230
+ width: 100%;
2231
+ }
2232
+
2233
+ .offline-code-value:hover {
2234
+ border-color: #3b82f6;
2235
+ box-shadow: 0 2px 6px rgba(59, 130, 246, 0.1);
2236
+ }
2237
+
2238
+ .offline-copy-code-btn {
2239
+ position: absolute;
2240
+ right: 6px;
2241
+ top: 50%;
2242
+ transform: translateY(-50%);
2243
+ background: none;
2244
+ border: none;
2245
+ padding: 5px 8px;
2246
+ cursor: pointer;
2247
+ color: #6b7280;
2248
+ display: flex;
2249
+ align-items: center;
2250
+ justify-content: center;
2251
+ transition: all 0.2s;
2252
+ border-radius: 4px;
2253
+ }
2254
+
2255
+ .offline-copy-code-btn:hover {
2256
+ color: #3b82f6;
2257
+ background: #eff6ff;
2258
+ }
2259
+
2260
+ .offline-expiry-info {
2261
+ font-size: 11px;
2262
+ color: #6b7280;
2263
+ text-align: center;
2264
+ padding: 6px 0;
2265
+ }
2266
+
2267
+ .offline-expiry-label {
2268
+ font-weight: 700;
2269
+ text-transform: uppercase;
2270
+ letter-spacing: 0.5px;
2271
+ color: #1f2937;
2272
+ }
2273
+
2274
+ .offline-expiry-value {
2275
+ font-weight: 600;
2276
+ color: #374151;
2277
+ }
2278
+
2279
+ .offline-actions-row {
2280
+ display: flex;
2281
+ gap: 10px;
2282
+ }
2283
+
2284
+ .offline-download-qr-btn,
2285
+ .offline-online-checkout-btn {
2286
+ flex: 1;
2287
+ padding: 11px 16px;
2288
+ border: none;
2289
+ border-radius: 8px;
2290
+ font-size: 13px;
2291
+ font-weight: 700;
2292
+ cursor: pointer;
2293
+ transition: all 0.2s;
2294
+ text-transform: uppercase;
2295
+ letter-spacing: 0.4px;
2296
+ }
2297
+
2298
+ .offline-download-qr-btn {
2299
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
2300
+ color: white;
2301
+ box-shadow: 0 2px 6px rgba(59, 130, 246, 0.2);
2302
+ }
2303
+
2304
+ .offline-download-qr-btn:hover:not(:disabled) {
2305
+ background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
2306
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
2307
+ transform: translateY(-1px);
2308
+ }
2309
+
2310
+ .offline-online-checkout-btn {
2311
+ background: linear-gradient(135deg, #111827 0%, #1f2937 100%);
2312
+ color: white;
2313
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
2314
+ }
2315
+
2316
+ .offline-online-checkout-btn:hover {
2317
+ background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
2318
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
2319
+ transform: translateY(-1px);
2320
+ }
2321
+
2322
+ .offline-download-qr-btn:active:not(:disabled),
2323
+ .offline-online-checkout-btn:active {
2324
+ transform: translateY(0);
2325
+ }
2326
+
2327
+ .offline-download-qr-btn:disabled {
2328
+ opacity: 0.7;
2329
+ cursor: not-allowed;
2330
+ }
2331
+ `;
2332
+ document.head.appendChild(style);
1773
2333
  }
1774
2334
  /**
1775
2335
  * Create offer section
@@ -2408,24 +2968,27 @@ class LobbyModal {
2408
2968
  groupNumber: this.data.groupNumber,
2409
2969
  status: this.data.status,
2410
2970
  });
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}`);
2971
+ // Show entrance animation, then open modal after animation completes
2972
+ this.showEntranceAnimation(() => {
2973
+ this.modalElement = this.createModalStructure();
2974
+ document.body.appendChild(this.modalElement);
2975
+ // Subscribe to realtime socket events
2976
+ this.subscribeToSocketEvents();
2977
+ // If socket is available and connected, subscribe to this group
2978
+ if (this.socketManager && this.socketManager.isConnected() && this.currentGroupId) {
2979
+ this.logger.info(`[LobbyModal] Socket connected, subscribing to group ${this.currentGroupId}`);
2980
+ this.socketManager.subscribeToGroup(this.currentGroupId);
2981
+ }
2982
+ else if (this.socketManager && !this.socketManager.isConnected()) {
2983
+ this.logger.warn("[LobbyModal] Socket manager not connected yet");
2984
+ }
2985
+ // Start timers and animations
2986
+ this.startTimer();
2987
+ this.startActivityAnimation();
2988
+ // Track analytics
2989
+ if (this.analyticsClient) ;
2990
+ this.logger.info(`Lobby modal opened for product: ${this.data.productId}`);
2991
+ });
2429
2992
  }
2430
2993
  /**
2431
2994
  * Close the modal
@@ -2471,6 +3034,10 @@ class LobbyModal {
2471
3034
  this.updateLockUI(!!this.data.isLocked);
2472
3035
  this.updateTitleUI(this.data);
2473
3036
  this.updateLinkVisibility(!!this.data.isLocked);
3037
+ // Update offline redemption visibility if data changed
3038
+ if (data.offlineRedemption !== undefined || data.status !== undefined) {
3039
+ this.updateOfflineRedemptionVisibility();
3040
+ }
2474
3041
  // Update progress if changed
2475
3042
  if (data.progress !== undefined) {
2476
3043
  const progressFill = document.getElementById("progressFill");
@@ -2545,6 +3112,42 @@ class LobbyModal {
2545
3112
  return;
2546
3113
  linkSection.style.display = isLocked ? "" : "none";
2547
3114
  }
3115
+ /**
3116
+ * Update offline redemption visibility when group is fulfilled
3117
+ */
3118
+ updateOfflineRedemptionVisibility() {
3119
+ const connectedSection = document.getElementById("lobbyConnectedSection");
3120
+ if (!connectedSection)
3121
+ return;
3122
+ const isComplete = !this.computeIsLocked(this.data);
3123
+ const hasOfflineRedemption = this.data.offlineRedemption && isValidOfflineRedemption(this.data.offlineRedemption);
3124
+ // Get existing elements
3125
+ const existingOffline = connectedSection.querySelector(".offline-redemption-section");
3126
+ const existingLink = connectedSection.querySelector(".link-share-container");
3127
+ if (isComplete && hasOfflineRedemption) {
3128
+ // Show offline redemption, hide link/share
3129
+ if (existingLink) {
3130
+ existingLink.remove();
3131
+ }
3132
+ if (!existingOffline) {
3133
+ const offlineSection = this.createOfflineRedemptionSection(this.data.offlineRedemption);
3134
+ connectedSection.appendChild(offlineSection);
3135
+ }
3136
+ }
3137
+ else {
3138
+ // Show link/share, hide offline redemption
3139
+ if (existingOffline) {
3140
+ existingOffline.remove();
3141
+ }
3142
+ if (!existingLink) {
3143
+ const linkShareContainer = document.createElement("div");
3144
+ linkShareContainer.className = "link-share-container";
3145
+ const linkWp = this.createLinkSection();
3146
+ linkShareContainer.appendChild(linkWp);
3147
+ connectedSection.appendChild(linkShareContainer);
3148
+ }
3149
+ }
3150
+ }
2548
3151
  /**
2549
3152
  * Subscribe to socket events
2550
3153
  */
@@ -2581,7 +3184,7 @@ class LobbyModal {
2581
3184
  break;
2582
3185
  case "group:fulfilled":
2583
3186
  emoji = "🎁";
2584
- action = "unlocked the reward – group filled!";
3187
+ action = "unlocked the discount – group filled!";
2585
3188
  break;
2586
3189
  case "group:created":
2587
3190
  emoji = "🛒";
@@ -2649,6 +3252,405 @@ class LobbyModal {
2649
3252
  }
2650
3253
  }
2651
3254
 
3255
+ /**
3256
+ * Offline Redemption Modal
3257
+ * Displays QR code and redemption code for fulfilled groups
3258
+ */
3259
+ class OfflineRedemptionModal {
3260
+ constructor(offlineRedemption, onClose) {
3261
+ this.modalElement = null;
3262
+ this.offlineRedemption = offlineRedemption;
3263
+ this.onClose = onClose;
3264
+ }
3265
+ /**
3266
+ * Open the modal
3267
+ */
3268
+ open() {
3269
+ if (this.modalElement && document.body.contains(this.modalElement)) {
3270
+ return; // Already open
3271
+ }
3272
+ this.modalElement = this.createModalStructure();
3273
+ document.body.appendChild(this.modalElement);
3274
+ // Trigger entrance animation
3275
+ requestAnimationFrame(() => {
3276
+ var _a;
3277
+ (_a = this.modalElement) === null || _a === void 0 ? void 0 : _a.classList.add("offline-redemption-modal-open");
3278
+ });
3279
+ }
3280
+ /**
3281
+ * Close the modal
3282
+ */
3283
+ close() {
3284
+ if (!this.modalElement)
3285
+ return;
3286
+ this.modalElement.classList.remove("offline-redemption-modal-open");
3287
+ // Remove after animation completes
3288
+ setTimeout(() => {
3289
+ var _a;
3290
+ if (this.modalElement && document.body.contains(this.modalElement)) {
3291
+ document.body.removeChild(this.modalElement);
3292
+ this.modalElement = null;
3293
+ }
3294
+ (_a = this.onClose) === null || _a === void 0 ? void 0 : _a.call(this);
3295
+ }, 300);
3296
+ }
3297
+ /**
3298
+ * Create the modal DOM structure
3299
+ */
3300
+ createModalStructure() {
3301
+ const container = document.createElement("div");
3302
+ container.className = "offline-redemption-modal-overlay";
3303
+ // Backdrop
3304
+ const backdrop = document.createElement("div");
3305
+ backdrop.className = "offline-redemption-backdrop";
3306
+ backdrop.addEventListener("click", () => this.close());
3307
+ // Modal content
3308
+ const modal = document.createElement("div");
3309
+ modal.className = "offline-redemption-modal";
3310
+ // Close button
3311
+ const closeBtn = document.createElement("button");
3312
+ closeBtn.className = "offline-redemption-close-btn";
3313
+ closeBtn.setAttribute("aria-label", "Close modal");
3314
+ closeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>`;
3315
+ closeBtn.addEventListener("click", () => this.close());
3316
+ // Header
3317
+ const header = document.createElement("div");
3318
+ header.className = "offline-redemption-header";
3319
+ const title = document.createElement("h2");
3320
+ title.className = "offline-redemption-title";
3321
+ title.textContent = "Redeem In-Store";
3322
+ header.appendChild(title);
3323
+ header.appendChild(closeBtn);
3324
+ // Body
3325
+ const body = document.createElement("div");
3326
+ body.className = "offline-redemption-body";
3327
+ // QR Code section
3328
+ const qrSection = document.createElement("div");
3329
+ qrSection.className = "offline-redemption-qr-section";
3330
+ const qrImage = document.createElement("img");
3331
+ qrImage.className = "offline-redemption-qr-image";
3332
+ qrImage.alt = "Redemption QR Code";
3333
+ qrImage.src = buildQRUrl(this.offlineRedemption.qr_code_data);
3334
+ // QR image fallback handler
3335
+ qrImage.addEventListener("error", () => {
3336
+ qrImage.style.display = "none";
3337
+ const errorMsg = document.createElement("div");
3338
+ errorMsg.className = "offline-redemption-qr-error";
3339
+ errorMsg.textContent = "QR code unavailable";
3340
+ qrSection.appendChild(errorMsg);
3341
+ });
3342
+ qrSection.appendChild(qrImage);
3343
+ // Redemption code section
3344
+ const codeSection = document.createElement("div");
3345
+ codeSection.className = "offline-redemption-code-section";
3346
+ const codeLabel = document.createElement("p");
3347
+ codeLabel.className = "offline-redemption-code-label";
3348
+ codeLabel.textContent = "Redemption Code:";
3349
+ const codeValue = document.createElement("div");
3350
+ codeValue.className = "offline-redemption-code-value";
3351
+ codeValue.textContent = this.offlineRedemption.redemption_code;
3352
+ codeSection.appendChild(codeLabel);
3353
+ codeSection.appendChild(codeValue);
3354
+ // Expiry section
3355
+ const expirySection = document.createElement("div");
3356
+ expirySection.className = "offline-redemption-expiry-section";
3357
+ const expiryLabel = document.createElement("p");
3358
+ expiryLabel.className = "offline-redemption-expiry-label";
3359
+ expiryLabel.textContent = "Valid Until:";
3360
+ const expiryValue = document.createElement("p");
3361
+ expiryValue.className = "offline-redemption-expiry-value";
3362
+ const isValid = isRedemptionValid(this.offlineRedemption.offline_expires_at);
3363
+ expiryValue.textContent = formatExpiryDate(this.offlineRedemption.offline_expires_at);
3364
+ if (!isValid) {
3365
+ expiryValue.classList.add("expired");
3366
+ }
3367
+ expirySection.appendChild(expiryLabel);
3368
+ expirySection.appendChild(expiryValue);
3369
+ body.appendChild(qrSection);
3370
+ body.appendChild(codeSection);
3371
+ body.appendChild(expirySection);
3372
+ // Actions
3373
+ const actions = document.createElement("div");
3374
+ actions.className = "offline-redemption-actions";
3375
+ const downloadBtn = document.createElement("button");
3376
+ downloadBtn.className = "offline-redemption-action-btn download-btn";
3377
+ downloadBtn.textContent = "Download QR";
3378
+ downloadBtn.addEventListener("click", () => this.handleDownloadQR(downloadBtn));
3379
+ const copyBtn = document.createElement("button");
3380
+ copyBtn.className = "offline-redemption-action-btn copy-btn";
3381
+ copyBtn.textContent = "Copy Code";
3382
+ copyBtn.addEventListener("click", () => this.handleCopyCode(copyBtn));
3383
+ actions.appendChild(downloadBtn);
3384
+ actions.appendChild(copyBtn);
3385
+ // Assemble modal
3386
+ modal.appendChild(header);
3387
+ modal.appendChild(body);
3388
+ modal.appendChild(actions);
3389
+ container.appendChild(backdrop);
3390
+ container.appendChild(modal);
3391
+ // Inject styles
3392
+ this.injectStyles();
3393
+ return container;
3394
+ }
3395
+ /**
3396
+ * Handle download QR button click
3397
+ */
3398
+ async handleDownloadQR(button) {
3399
+ const originalText = button.textContent;
3400
+ button.disabled = true;
3401
+ button.textContent = "Downloading...";
3402
+ const success = await downloadQRCode(buildQRUrl(this.offlineRedemption.qr_code_data), "redemption-qr.png");
3403
+ button.textContent = success ? "Downloaded!" : "Download Failed";
3404
+ setTimeout(() => {
3405
+ button.textContent = originalText;
3406
+ button.disabled = false;
3407
+ }, 2000);
3408
+ }
3409
+ /**
3410
+ * Handle copy code button click
3411
+ */
3412
+ async handleCopyCode(button) {
3413
+ const originalText = button.textContent;
3414
+ button.disabled = true;
3415
+ const success = await copyRedemptionCode(this.offlineRedemption.redemption_code);
3416
+ button.textContent = success ? "Copied!" : "Copy Failed";
3417
+ setTimeout(() => {
3418
+ button.textContent = originalText;
3419
+ button.disabled = false;
3420
+ }, 2000);
3421
+ }
3422
+ /**
3423
+ * Inject modal styles
3424
+ */
3425
+ injectStyles() {
3426
+ // Check if styles already injected
3427
+ if (document.getElementById("offline-redemption-modal-styles")) {
3428
+ return;
3429
+ }
3430
+ const style = document.createElement("style");
3431
+ style.id = "offline-redemption-modal-styles";
3432
+ style.textContent = `
3433
+ .offline-redemption-modal-overlay {
3434
+ position: fixed;
3435
+ top: 0;
3436
+ left: 0;
3437
+ right: 0;
3438
+ bottom: 0;
3439
+ display: flex;
3440
+ align-items: center;
3441
+ justify-content: center;
3442
+ z-index: 10000;
3443
+ opacity: 0;
3444
+ transition: opacity 0.3s ease-out;
3445
+ }
3446
+
3447
+ .offline-redemption-modal-overlay.offline-redemption-modal-open {
3448
+ opacity: 1;
3449
+ }
3450
+
3451
+ .offline-redemption-backdrop {
3452
+ position: absolute;
3453
+ top: 0;
3454
+ left: 0;
3455
+ right: 0;
3456
+ bottom: 0;
3457
+ background: rgba(0, 0, 0, 0.5);
3458
+ cursor: pointer;
3459
+ }
3460
+
3461
+ .offline-redemption-modal {
3462
+ position: relative;
3463
+ z-index: 10001;
3464
+ background: white;
3465
+ border-radius: 12px;
3466
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
3467
+ max-width: 400px;
3468
+ width: 90%;
3469
+ max-height: 90vh;
3470
+ overflow-y: auto;
3471
+ animation: slideUp 0.3s ease-out;
3472
+ }
3473
+
3474
+ @keyframes slideUp {
3475
+ from {
3476
+ opacity: 0;
3477
+ transform: translateY(40px);
3478
+ }
3479
+ to {
3480
+ opacity: 1;
3481
+ transform: translateY(0);
3482
+ }
3483
+ }
3484
+
3485
+ .offline-redemption-header {
3486
+ display: flex;
3487
+ justify-content: space-between;
3488
+ align-items: center;
3489
+ padding: 20px;
3490
+ border-bottom: 1px solid #f0f0f0;
3491
+ }
3492
+
3493
+ .offline-redemption-title {
3494
+ margin: 0;
3495
+ font-size: 20px;
3496
+ font-weight: 600;
3497
+ color: #111827;
3498
+ }
3499
+
3500
+ .offline-redemption-close-btn {
3501
+ background: none;
3502
+ border: none;
3503
+ padding: 0;
3504
+ cursor: pointer;
3505
+ display: flex;
3506
+ align-items: center;
3507
+ justify-content: center;
3508
+ color: #6b7280;
3509
+ transition: color 0.2s;
3510
+ }
3511
+
3512
+ .offline-redemption-close-btn:hover {
3513
+ color: #111827;
3514
+ }
3515
+
3516
+ .offline-redemption-close-btn svg {
3517
+ width: 24px;
3518
+ height: 24px;
3519
+ }
3520
+
3521
+ .offline-redemption-body {
3522
+ padding: 24px 20px;
3523
+ display: flex;
3524
+ flex-direction: column;
3525
+ gap: 20px;
3526
+ }
3527
+
3528
+ .offline-redemption-qr-section {
3529
+ display: flex;
3530
+ justify-content: center;
3531
+ background: #f9fafb;
3532
+ padding: 16px;
3533
+ border-radius: 8px;
3534
+ min-height: 200px;
3535
+ align-items: center;
3536
+ }
3537
+
3538
+ .offline-redemption-qr-image {
3539
+ max-width: 100%;
3540
+ max-height: 200px;
3541
+ width: auto;
3542
+ height: auto;
3543
+ }
3544
+
3545
+ .offline-redemption-qr-error {
3546
+ text-align: center;
3547
+ color: #6b7280;
3548
+ font-size: 14px;
3549
+ }
3550
+
3551
+ .offline-redemption-code-section,
3552
+ .offline-redemption-expiry-section {
3553
+ display: flex;
3554
+ flex-direction: column;
3555
+ gap: 8px;
3556
+ }
3557
+
3558
+ .offline-redemption-code-label,
3559
+ .offline-redemption-expiry-label {
3560
+ margin: 0;
3561
+ font-size: 12px;
3562
+ font-weight: 600;
3563
+ color: #6b7280;
3564
+ text-transform: uppercase;
3565
+ letter-spacing: 0.5px;
3566
+ }
3567
+
3568
+ .offline-redemption-code-value {
3569
+ font-family: monospace;
3570
+ font-size: 16px;
3571
+ font-weight: 600;
3572
+ color: #111827;
3573
+ padding: 12px;
3574
+ background: #f9fafb;
3575
+ border: 1px solid #e5e7eb;
3576
+ border-radius: 6px;
3577
+ text-align: center;
3578
+ letter-spacing: 2px;
3579
+ }
3580
+
3581
+ .offline-redemption-expiry-value {
3582
+ margin: 0;
3583
+ font-size: 14px;
3584
+ color: #374151;
3585
+ }
3586
+
3587
+ .offline-redemption-expiry-value.expired {
3588
+ color: #dc2626;
3589
+ font-weight: 600;
3590
+ }
3591
+
3592
+ .offline-redemption-actions {
3593
+ display: flex;
3594
+ gap: 12px;
3595
+ padding: 20px;
3596
+ border-top: 1px solid #f0f0f0;
3597
+ }
3598
+
3599
+ .offline-redemption-action-btn {
3600
+ flex: 1;
3601
+ padding: 12px 16px;
3602
+ border: none;
3603
+ border-radius: 6px;
3604
+ font-size: 14px;
3605
+ font-weight: 600;
3606
+ cursor: pointer;
3607
+ transition: all 0.2s;
3608
+ }
3609
+
3610
+ .download-btn {
3611
+ background: #3b82f6;
3612
+ color: white;
3613
+ }
3614
+
3615
+ .download-btn:hover:not(:disabled) {
3616
+ background: #2563eb;
3617
+ }
3618
+
3619
+ .copy-btn {
3620
+ background: #f3f4f6;
3621
+ color: #111827;
3622
+ border: 1px solid #e5e7eb;
3623
+ }
3624
+
3625
+ .copy-btn:hover:not(:disabled) {
3626
+ background: #e5e7eb;
3627
+ }
3628
+
3629
+ .offline-redemption-action-btn:disabled {
3630
+ opacity: 0.7;
3631
+ cursor: not-allowed;
3632
+ }
3633
+
3634
+ /* Mobile responsiveness */
3635
+ @media (max-width: 480px) {
3636
+ .offline-redemption-modal {
3637
+ width: 95%;
3638
+ max-height: 95vh;
3639
+ }
3640
+
3641
+ .offline-redemption-body {
3642
+ padding: 16px;
3643
+ }
3644
+
3645
+ .offline-redemption-actions {
3646
+ padding: 16px;
3647
+ }
3648
+ }
3649
+ `;
3650
+ document.head.appendChild(style);
3651
+ }
3652
+ }
3653
+
2652
3654
  /// <reference lib="dom" />
2653
3655
  /**
2654
3656
  * Widget state enumeration for tracking render lifecycle
@@ -2683,6 +3685,8 @@ class WidgetRoot {
2683
3685
  this.lastGroupDataRefreshTime = 0;
2684
3686
  this.GROUP_REFRESH_DEBOUNCE = 1000; // 1 second min between refreshes
2685
3687
  this.groupExpiryRefreshTriggered = false; // Track if expiry refresh already triggered for current group
3688
+ this.offlineRedemption = null;
3689
+ this.offlineRedemptionModal = null;
2686
3690
  /** Handle backend fulfillment notifications */
2687
3691
  this.handleGroupFulfilledEvent = (event) => {
2688
3692
  const detail = event.detail;
@@ -2839,11 +3843,22 @@ class WidgetRoot {
2839
3843
  }
2840
3844
  /** Persist fulfilled state, emit callback, and refresh UI */
2841
3845
  processGroupFulfilled(eventData) {
2842
- var _a, _b;
3846
+ var _a, _b, _c, _d;
2843
3847
  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);
3848
+ // Extract reward from new event structure or fallback to legacy
3849
+ const reward = ((_a = eventData.frozen_reward) === null || _a === void 0 ? void 0 : _a.reward) || eventData.reward || null;
3850
+ this.frozenReward = reward;
3851
+ // Extract group ID from new event structure or fallback to legacy
3852
+ this.currentGroupId = ((_b = eventData.group) === null || _b === void 0 ? void 0 : _b.id) || eventData.groupId || null;
3853
+ // Store offline redemption data if available
3854
+ if (eventData.offline_redemption && isValidOfflineRedemption(eventData.offline_redemption)) {
3855
+ this.offlineRedemption = eventData.offline_redemption;
3856
+ this.logger.info("Offline redemption data stored", {
3857
+ member_id: this.offlineRedemption.member_id,
3858
+ code: this.offlineRedemption.redemption_code,
3859
+ });
3860
+ }
3861
+ (_d = (_c = this.events) === null || _c === void 0 ? void 0 : _c.onGroupFulfilled) === null || _d === void 0 ? void 0 : _d.call(_c, eventData);
2847
3862
  this.renderFulfilledState();
2848
3863
  }
2849
3864
  /** Re-render widget and external containers to reflect fulfillment */
@@ -2958,16 +3973,42 @@ class WidgetRoot {
2958
3973
  rewardCheck.textContent = "✔";
2959
3974
  rewardCheck.style.color = "var(--cobuy-success-color, #10b981)";
2960
3975
  const rewardLabel = document.createElement("span");
2961
- rewardLabel.textContent = rewardText ? `Reward available: ${rewardText}` : "Reward available";
3976
+ rewardLabel.textContent = rewardText
3977
+ ? `Discount available: ${rewardText}`
3978
+ : "Discount available";
2962
3979
  rewardLine.appendChild(rewardCheck);
2963
3980
  rewardLine.appendChild(rewardLabel);
2964
3981
  root.appendChild(title);
2965
3982
  root.appendChild(rewardLine);
2966
- // Create footer with View all Groups link at bottom right
3983
+ // Create footer with action CTAs
2967
3984
  const footer = document.createElement("div");
2968
3985
  footer.style.display = "flex";
2969
3986
  footer.style.justifyContent = "flex-end";
2970
3987
  footer.style.alignItems = "center";
3988
+ footer.style.gap = "12px";
3989
+ // Add "Redeem In-store" CTA if offline redemption is available
3990
+ if (this.offlineRedemption && isValidOfflineRedemption(this.offlineRedemption)) {
3991
+ const redeemLink = document.createElement("button");
3992
+ redeemLink.className = "cobuy-redeem-instore-link";
3993
+ redeemLink.style.background = "none";
3994
+ redeemLink.style.border = "none";
3995
+ redeemLink.style.color = "var(--cobuy-primary-color, #3b82f6)";
3996
+ redeemLink.style.cursor = "pointer";
3997
+ redeemLink.style.fontSize = "12px";
3998
+ redeemLink.style.fontWeight = "600";
3999
+ redeemLink.style.textDecoration = "underline";
4000
+ redeemLink.style.padding = "0";
4001
+ redeemLink.style.transition = "opacity 0.2s";
4002
+ redeemLink.textContent = "Redeem In-store";
4003
+ redeemLink.addEventListener("click", () => this.openOfflineRedemptionModal());
4004
+ redeemLink.addEventListener("mouseover", () => {
4005
+ redeemLink.style.opacity = "0.7";
4006
+ });
4007
+ redeemLink.addEventListener("mouseout", () => {
4008
+ redeemLink.style.opacity = "1";
4009
+ });
4010
+ footer.appendChild(redeemLink);
4011
+ }
2971
4012
  root.appendChild(footer);
2972
4013
  return root;
2973
4014
  }
@@ -3151,6 +4192,12 @@ class WidgetRoot {
3151
4192
  this.currentGroupData = groupData;
3152
4193
  this.currentGroupId = (groupData === null || groupData === void 0 ? void 0 : groupData.id) || null;
3153
4194
  this.currentRewardData = rewardData;
4195
+ console.log("group data for product ", options.productId, groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption, isValidOfflineRedemption(groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption));
4196
+ // Extract offline_redemption from API response if available
4197
+ if ((groupData === null || groupData === void 0 ? void 0 : groupData.offline_redemption) &&
4198
+ isValidOfflineRedemption(groupData.offline_redemption)) {
4199
+ this.offlineRedemption = groupData.offline_redemption;
4200
+ }
3154
4201
  // Check if group is already fulfilled (full) on initial load
3155
4202
  if (groupData) {
3156
4203
  const participants = Number(groupData.participants_count || 0);
@@ -3569,16 +4616,14 @@ class WidgetRoot {
3569
4616
  // rewardText ? `Reward locked in: ${rewardText}` : "Reward available for this group",
3570
4617
  // );
3571
4618
  // rewardLine.title = rewardLine.textContent;
3572
- // }
3573
- // else if (activeReward) {
4619
+ // } else if (activeReward) {
3574
4620
  // const rewardText = this.formatRewardText(activeReward);
3575
4621
  // rewardLine.textContent = rewardText
3576
4622
  // ? `Save up to ${rewardText} with CoBuy`
3577
4623
  // : "CoBuy reward available";
3578
4624
  // rewardLine.setAttribute("aria-label", `Eligible for CoBuy reward: ${rewardLine.textContent}`);
3579
4625
  // rewardLine.title = rewardLine.textContent;
3580
- // }
3581
- // else {
4626
+ // } else {
3582
4627
  // rewardLine.textContent = "CoBuy offer loading or unavailable";
3583
4628
  // rewardLine.setAttribute("aria-label", "CoBuy offer loading or unavailable");
3584
4629
  // rewardLine.title = "CoBuy offer loading or unavailable";
@@ -3814,6 +4859,12 @@ class WidgetRoot {
3814
4859
  }
3815
4860
  groupJoinData = joinResponse.data;
3816
4861
  this.logger.info("Successfully joined group", groupJoinData);
4862
+ this.offlineRedemption =
4863
+ groupJoinData &&
4864
+ groupJoinData.offline_redemption &&
4865
+ isValidOfflineRedemption(groupJoinData.offline_redemption)
4866
+ ? groupJoinData.offline_redemption
4867
+ : null;
3817
4868
  // Trigger invite tracking before opening lobby (global for product)
3818
4869
  try {
3819
4870
  const inviteResponse = await this.apiClient.inviteToGroup(this.currentGroupId, "copy_link");
@@ -3859,6 +4910,13 @@ class WidgetRoot {
3859
4910
  const isGroupFulfilled = groupJoinData.group.participants_count >= groupJoinData.group.max_participants;
3860
4911
  const groupLinkFromInvite = inviteData === null || inviteData === void 0 ? void 0 : inviteData.invite_url;
3861
4912
  const shareMessageFromInvite = inviteData === null || inviteData === void 0 ? void 0 : inviteData.share_message;
4913
+ console.log("offlien dataaa", groupJoinData.offline_redemption, isValidOfflineRedemption(groupJoinData.offline_redemption));
4914
+ // Extract offline_redemption from join response if available
4915
+ const offlineRedemptionFromJoin = groupJoinData.offline_redemption &&
4916
+ isValidOfflineRedemption(groupJoinData.offline_redemption)
4917
+ ? groupJoinData.offline_redemption
4918
+ : undefined;
4919
+ this.offlineRedemption = offlineRedemptionFromJoin || null;
3862
4920
  // Prepare modal data with real values from join response
3863
4921
  cobuySDK.openModal({
3864
4922
  productId,
@@ -3877,6 +4935,7 @@ class WidgetRoot {
3877
4935
  groupLink: groupLinkFromInvite || `https://cobuy.group/lobby/${groupJoinData.group.id}`,
3878
4936
  shareMessage: shareMessageFromInvite,
3879
4937
  isLocked: !isGroupFulfilled,
4938
+ offlineRedemption: offlineRedemptionFromJoin,
3880
4939
  activities: [
3881
4940
  {
3882
4941
  emoji: "👤",
@@ -3926,6 +4985,20 @@ class WidgetRoot {
3926
4985
  }
3927
4986
  }
3928
4987
  }
4988
+ /**
4989
+ * Open offline redemption modal when user clicks "Redeem In-store"
4990
+ */
4991
+ openOfflineRedemptionModal() {
4992
+ if (!this.offlineRedemption || !isValidOfflineRedemption(this.offlineRedemption)) {
4993
+ this.logger.warn("Offline redemption data not available");
4994
+ return;
4995
+ }
4996
+ this.logger.info("Opening offline redemption modal");
4997
+ this.offlineRedemptionModal = new OfflineRedemptionModal(this.offlineRedemption, () => {
4998
+ this.offlineRedemptionModal = null;
4999
+ });
5000
+ this.offlineRedemptionModal.open();
5001
+ }
3929
5002
  /**
3930
5003
  * Emit checkout intent without performing navigation
3931
5004
  */
@@ -3984,13 +5057,13 @@ class WidgetRoot {
3984
5057
  const reward = this.frozenReward || (rewardData === null || rewardData === void 0 ? void 0 : rewardData.reward);
3985
5058
  if (this.groupFulfilled) {
3986
5059
  const rewardText = this.formatRewardText(reward);
3987
- return rewardText ? `Reward available: ${rewardText}` : "Reward available for this group";
5060
+ return rewardText ? `Discount available: ${rewardText}` : "Discount available for this group";
3988
5061
  }
3989
5062
  if (!reward) {
3990
- return "Join with CoBuy to unlock rewards";
5063
+ return "Join with CoBuy to unlock discount";
3991
5064
  }
3992
5065
  const rewardText = this.formatRewardText(reward);
3993
- return rewardText ? `Save up to ${rewardText} with CoBuy` : "CoBuy reward available";
5066
+ return rewardText ? `Save up to ${rewardText} with CoBuy` : "CoBuy discount available";
3994
5067
  }
3995
5068
  /**
3996
5069
  * Public hook to request a realtime refresh from external callers
@@ -4588,7 +5661,7 @@ class AnalyticsClient {
4588
5661
  */
4589
5662
  async trackCtaClick(productId) {
4590
5663
  const event = {
4591
- event: "cta_clicked",
5664
+ event: "CTA_CLICKED",
4592
5665
  productId,
4593
5666
  timestamp: new Date().toISOString(),
4594
5667
  sessionId: this.sessionId,
@@ -4632,6 +5705,28 @@ class AnalyticsClient {
4632
5705
  throw error;
4633
5706
  }
4634
5707
  }
5708
+ /**
5709
+ * Track page view event
5710
+ */
5711
+ async trackPageView() {
5712
+ const event = {
5713
+ event: "PAGE_VIEW",
5714
+ timestamp: new Date().toISOString(),
5715
+ context: {
5716
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
5717
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
5718
+ sdkVersion: this.sdkVersion,
5719
+ },
5720
+ };
5721
+ try {
5722
+ await this.sendEvent(event);
5723
+ this.logger.info("[Analytics] Page view tracked");
5724
+ }
5725
+ catch (error) {
5726
+ // Log but don't throw - analytics failure should not break UX
5727
+ this.logger.error("[Analytics] Failed to track page view", error);
5728
+ }
5729
+ }
4635
5730
  /**
4636
5731
  * Track custom event (extensible for future use)
4637
5732
  */
@@ -8819,9 +9914,10 @@ Object.assign(lookup, {
8819
9914
  * Manages a single socket.io connection for the SDK
8820
9915
  */
8821
9916
  class SocketManager {
8822
- constructor(config, sdkVersion) {
9917
+ constructor(config, sdkVersion, sessionId) {
8823
9918
  this.config = config;
8824
9919
  this.sdkVersion = sdkVersion;
9920
+ this.sessionId = sessionId;
8825
9921
  this.socket = null;
8826
9922
  this.logger = new Logger(config.debug);
8827
9923
  }
@@ -8866,6 +9962,7 @@ class SocketManager {
8866
9962
  auth: {
8867
9963
  merchantKey: this.config.merchantKey,
8868
9964
  sdkVersion: this.sdkVersion,
9965
+ sessionId: this.sessionId,
8869
9966
  },
8870
9967
  autoConnect: true,
8871
9968
  });
@@ -9052,7 +10149,9 @@ class CoBuy {
9052
10149
  const ref = window.localStorage.getItem(key);
9053
10150
  if (ref) {
9054
10151
  this.logger.debug(`[SDK] Retrieved checkout reference via prefix: ${key}`);
9055
- const parsedGroupId = key.startsWith(`${basePrefix}_`) ? key.substring(basePrefix.length + 1) : null;
10152
+ const parsedGroupId = key.startsWith(`${basePrefix}_`)
10153
+ ? key.substring(basePrefix.length + 1)
10154
+ : null;
9056
10155
  return { key, checkoutRef: ref, groupId: parsedGroupId };
9057
10156
  }
9058
10157
  }
@@ -9142,7 +10241,7 @@ class CoBuy {
9142
10241
  // Initialize real-time socket connection for public mode
9143
10242
  if (config.authMode === "public" && config.merchantKey) {
9144
10243
  try {
9145
- this.socketManager = new SocketManager(config, SDK_VERSION);
10244
+ this.socketManager = new SocketManager(config, SDK_VERSION, this.sessionId);
9146
10245
  this.socketManager.connect();
9147
10246
  this.socketManager.bindHandlers({
9148
10247
  onGroupFulfilled: (payload) => {
@@ -9184,6 +10283,13 @@ class CoBuy {
9184
10283
  this.logger.warn("[SDK] Failed to initialize sockets", e);
9185
10284
  }
9186
10285
  }
10286
+ // Track page view event after successful initialization
10287
+ if (this.analyticsClient) {
10288
+ this.analyticsClient.trackPageView().catch((error) => {
10289
+ // Non-blocking: Analytics failure should not affect SDK initialization
10290
+ this.logger.warn("[SDK] Failed to track page view", error);
10291
+ });
10292
+ }
9187
10293
  this.logger.info("SDK initialization complete");
9188
10294
  }
9189
10295
  catch (error) {
@@ -9263,6 +10369,7 @@ class CoBuy {
9263
10369
  groupLink: options.groupLink,
9264
10370
  activities: options.activities,
9265
10371
  isLocked: options.isLocked,
10372
+ offlineRedemption: options.offlineRedemption,
9266
10373
  };
9267
10374
  // Create modal instance
9268
10375
  const modal = new LobbyModal(modalData, {