@cshah18/sdk 4.13.0 → 4.15.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.
@@ -455,6 +455,8 @@ var CoBuySDK = (function (exports) {
455
455
  debug,
456
456
  events: options.events,
457
457
  performance,
458
+ geo: options.geo,
459
+ device: options.device,
458
460
  };
459
461
  return this.config;
460
462
  }
@@ -822,6 +824,7 @@ var CoBuySDK = (function (exports) {
822
824
  this.currentJoinedGroupId = null;
823
825
  this.currentProductId = null;
824
826
  this.currentSessionId = null;
827
+ this.analyticsClient = null;
825
828
  this.onGroupJoined = null;
826
829
  this.onViewProgress = null;
827
830
  this.socketListenerRegistered = false;
@@ -836,6 +839,8 @@ var CoBuySDK = (function (exports) {
836
839
  /** Handle group member joined socket event and update groups list */
837
840
  this.onGroupMemberJoined = (event) => {
838
841
  const detail = event.detail || {};
842
+ console.log("eventttttt", detail);
843
+ const eventType = event.type;
839
844
  const productId = detail.product_id;
840
845
  const groupData = detail.group;
841
846
  // Only process if this is for the current product
@@ -857,6 +862,16 @@ var CoBuySDK = (function (exports) {
857
862
  if (typeof participantsCount === "number") {
858
863
  this.groups[groupIndex].joined = participantsCount;
859
864
  }
865
+ // If this session left the currently joined group, clear stale local membership state.
866
+ const leavingSessionId = detail.session_id;
867
+ if (eventType === "group:member:left" &&
868
+ this.currentSessionId &&
869
+ leavingSessionId === this.currentSessionId &&
870
+ this.currentJoinedGroupId === groupId) {
871
+ this.currentJoinedGroupId = null;
872
+ this.groups[groupIndex].isMember = false;
873
+ this.updateStartButtonState(false, this.isLoading);
874
+ }
860
875
  this.logger.info(`[GroupListModal] Updated group ${groupId} - participants: ${participantsCount}`);
861
876
  // Re-render the specific group card
862
877
  this.updateGroupCard(groupId);
@@ -868,6 +883,10 @@ var CoBuySDK = (function (exports) {
868
883
  this.apiClient = apiClient;
869
884
  this.injectStyles();
870
885
  }
886
+ /** Set the analytics client for event tracking */
887
+ setAnalyticsClient(client) {
888
+ this.analyticsClient = client;
889
+ }
871
890
  /** Set callback for when a group is joined successfully */
872
891
  setOnGroupJoined(callback) {
873
892
  this.onGroupJoined = callback;
@@ -894,6 +913,7 @@ var CoBuySDK = (function (exports) {
894
913
  return;
895
914
  }
896
915
  window.addEventListener("group:member:joined", this.handleGroupMemberJoinedEvent);
916
+ window.addEventListener("group:member:left", this.handleGroupMemberJoinedEvent);
897
917
  this.socketListenerRegistered = true;
898
918
  this.logger.debug("[GroupListModal] Socket event listeners registered");
899
919
  }
@@ -903,6 +923,7 @@ var CoBuySDK = (function (exports) {
903
923
  return;
904
924
  }
905
925
  window.removeEventListener("group:member:joined", this.handleGroupMemberJoinedEvent);
926
+ window.removeEventListener("group:member:left", this.handleGroupMemberJoinedEvent);
906
927
  this.socketListenerRegistered = false;
907
928
  this.logger.debug("[GroupListModal] Socket event listeners unregistered");
908
929
  }
@@ -1034,13 +1055,11 @@ var CoBuySDK = (function (exports) {
1034
1055
  console.log("groupsss", groups);
1035
1056
  // If API reports membership, prefer it over cached state
1036
1057
  const memberGroup = groups.find((g) => g.isMember);
1037
- if (memberGroup) {
1038
- this.currentJoinedGroupId = memberGroup.groupId;
1039
- }
1058
+ this.currentJoinedGroupId = memberGroup ? memberGroup.groupId : null;
1040
1059
  this.groups = groups;
1041
1060
  this.liveCount = groups.length;
1042
1061
  this.logger.info(`Fetched ${groups.length} active groups`);
1043
- const hasMembership = Boolean(this.currentJoinedGroupId || groups.some((g) => g.isMember));
1062
+ const hasMembership = Boolean(memberGroup);
1044
1063
  this.updateStartButtonState(hasMembership, false);
1045
1064
  // Update and show live count display
1046
1065
  const liveCountText = document.getElementById("cobuy-live-count-text");
@@ -1125,10 +1144,21 @@ var CoBuySDK = (function (exports) {
1125
1144
  buttonElement.disabled = true;
1126
1145
  const originalText = button.textContent;
1127
1146
  button.textContent = "Joining...";
1147
+ const productId = this.currentProductId || "";
1148
+ if (this.analyticsClient) {
1149
+ this.analyticsClient
1150
+ .trackJoinAttempt(productId, groupId)
1151
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
1152
+ }
1128
1153
  try {
1129
1154
  const response = await this.apiClient.joinGroup(groupId);
1130
1155
  if (response.success && response.data) {
1131
1156
  this.logger.info(`[handleJoinGroup] Successfully joined group: ${groupId}`, response.data);
1157
+ if (this.analyticsClient) {
1158
+ this.analyticsClient
1159
+ .trackJoinSuccess(productId, groupId)
1160
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
1161
+ }
1132
1162
  // Track the joined group
1133
1163
  this.currentJoinedGroupId = groupId;
1134
1164
  // Close the modal and trigger callback with the full join response data
@@ -1139,12 +1169,22 @@ var CoBuySDK = (function (exports) {
1139
1169
  }
1140
1170
  else {
1141
1171
  this.logger.error("Failed to join group: API response unsuccessful");
1172
+ if (this.analyticsClient) {
1173
+ this.analyticsClient
1174
+ .trackJoinFailure(productId, groupId)
1175
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
1176
+ }
1142
1177
  button.textContent = originalText;
1143
1178
  buttonElement.disabled = false;
1144
1179
  }
1145
1180
  }
1146
1181
  catch (error) {
1147
1182
  this.logger.error("Error joining group", error);
1183
+ if (this.analyticsClient) {
1184
+ this.analyticsClient
1185
+ .trackJoinFailure(productId, groupId, "EXCEPTION", error instanceof Error ? error.message : String(error))
1186
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
1187
+ }
1148
1188
  button.textContent = originalText;
1149
1189
  buttonElement.disabled = false;
1150
1190
  }
@@ -1165,10 +1205,21 @@ var CoBuySDK = (function (exports) {
1165
1205
  this.setJoinButtonsDisabled(true);
1166
1206
  const originalText = button.textContent;
1167
1207
  button.textContent = "Creating...";
1208
+ const productId = this.currentProductId;
1209
+ if (this.analyticsClient) {
1210
+ this.analyticsClient
1211
+ .trackGroupCreateAttempt(productId)
1212
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
1213
+ }
1168
1214
  try {
1169
1215
  const response = await this.apiClient.createAndJoinGroup(this.currentProductId);
1170
1216
  if (response.success && response.data) {
1171
1217
  this.logger.info("Successfully created and joined new group", response.data);
1218
+ if (this.analyticsClient) {
1219
+ this.analyticsClient
1220
+ .trackGroupCreateSuccess(productId, response.data.group.id)
1221
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
1222
+ }
1172
1223
  this.currentJoinedGroupId = response.data.group.id;
1173
1224
  this.close();
1174
1225
  if (this.onGroupJoined) {
@@ -1177,6 +1228,11 @@ var CoBuySDK = (function (exports) {
1177
1228
  }
1178
1229
  else {
1179
1230
  this.logger.error("Failed to create group: API response unsuccessful");
1231
+ if (this.analyticsClient) {
1232
+ this.analyticsClient
1233
+ .trackGroupCreateFailure(productId)
1234
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
1235
+ }
1180
1236
  button.textContent = originalText;
1181
1237
  buttonElement.disabled = false;
1182
1238
  this.setJoinButtonsDisabled(false);
@@ -1184,6 +1240,11 @@ var CoBuySDK = (function (exports) {
1184
1240
  }
1185
1241
  catch (error) {
1186
1242
  this.logger.error("Error creating group", error);
1243
+ if (this.analyticsClient) {
1244
+ this.analyticsClient
1245
+ .trackGroupCreateFailure(productId, "EXCEPTION", error instanceof Error ? error.message : String(error))
1246
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
1247
+ }
1187
1248
  button.textContent = originalText;
1188
1249
  buttonElement.disabled = false;
1189
1250
  this.setJoinButtonsDisabled(false);
@@ -1356,7 +1417,7 @@ var CoBuySDK = (function (exports) {
1356
1417
  createGroupCard(group) {
1357
1418
  const card = document.createElement("div");
1358
1419
  card.className = "cobuy-group-card";
1359
- card.dataset.groupId = group.name || group.groupId;
1420
+ card.dataset.groupId = group.groupId;
1360
1421
  const header = document.createElement("div");
1361
1422
  header.className = "cobuy-group-card-header";
1362
1423
  const groupId = document.createElement("div");
@@ -2109,15 +2170,24 @@ var CoBuySDK = (function (exports) {
2109
2170
  }
2110
2171
  }
2111
2172
 
2112
- var css_248z = ".sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.cb-lobby-modal-container{height:100%;left:0;overflow-y:auto!important;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{align-items:center;backdrop-filter:blur(8px);background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);border-radius:24px;display:flex;gap:12px;margin-bottom:16px;padding:6px 12px;width:fit-content}.lobby-indicator-logo{display:block;flex-shrink:0;height:34px;width:auto}.pulsing-dot{align-items:center;display:flex;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:18px;margin:0!important}.share-btn svg{color:#fff;flex-shrink:0;height:18px;width:18px}@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)}.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: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:767px){.link-share-wrapper{flex-direction:column!important}}@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}";
2173
+ var css_248z = ".sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.cb-lobby-modal-container{height:100%;left:0;overflow-y:auto!important;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{align-items:center;backdrop-filter:blur(8px);background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);border-radius:24px;display:flex;gap:12px;margin-bottom:16px;padding:6px 12px;width:fit-content}.lobby-indicator-logo{display:block;flex-shrink:0;height:34px;width:auto}.pulsing-dot{align-items:center;display:flex;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)}.lobby-contact-protection-banner{align-items:center;background:#fffbeb;border:1px solid #f59e0b;border-radius:12px;display:flex;gap:12px;justify-content:space-between;padding:12px 14px}.lobby-contact-protection-copy{color:#7c2d12;font-size:13px;font-weight:600;line-height:1.4}.lobby-contact-protection-btn{background:#1e293b;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:12px;font-weight:700;padding:8px 12px;white-space:nowrap}.lobby-contact-protection-btn:hover{background:#0f172a}.lobby-protected-leave-container{align-items:center;background:#f0fdf4;border:1px solid #86efac;border-radius:12px;display:none;gap:12px;justify-content:space-between;padding:12px 14px}.lobby-protected-leave-copy{color:#166534;font-size:13px;font-weight:600;line-height:1.4}.lobby-protected-leave-btn{background:#b91c1c;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:12px;font-weight:700;padding:8px 12px;white-space:nowrap}.lobby-protected-leave-btn:hover{background:#991b1b}.lobby-contact-overlay{align-items:center;background:rgba(2,6,23,.5);display:flex;inset:0;justify-content:center;padding:20px;position:fixed;z-index:10020}.lobby-contact-card{background:#fff;border-radius:16px;box-shadow:0 12px 32px rgba(15,23,42,.25);display:flex;flex-direction:column;gap:12px;padding:20px;width:min(460px,100%)}.lobby-contact-title{color:#0f172a;font-size:22px;font-weight:800;line-height:1.2}.lobby-contact-subtitle{color:#334155;font-size:14px;line-height:1.5}.lobby-contact-input{border:1px solid #cbd5e1;border-radius:10px;font-size:14px;outline:none;padding:11px 12px;width:100%}.lobby-contact-input:focus{border-color:#1d4ed8;box-shadow:0 0 0 3px rgba(59,130,246,.2)}.lobby-contact-actions{align-items:center;display:flex;flex-wrap:wrap;gap:10px}.lobby-contact-danger,.lobby-contact-primary,.lobby-contact-secondary{border:none;border-radius:10px;cursor:pointer;font-size:13px;font-weight:700;padding:10px 14px}.lobby-contact-danger:disabled,.lobby-contact-primary:disabled,.lobby-contact-secondary:disabled{cursor:not-allowed;opacity:.7}.lobby-contact-primary.is-loading{padding-right:34px;position:relative}.lobby-contact-primary.is-loading:after{animation:lobby-spin .8s linear infinite;border:2px solid hsla(0,0%,100%,.45);border-radius:50%;border-top-color:#fff;content:\"\";height:14px;margin-top:-7px;position:absolute;right:12px;top:50%;width:14px}@keyframes lobby-spin{to{transform:rotate(1turn)}}.lobby-contact-primary{background:#1d4ed8;color:#fff}.lobby-contact-primary:hover{background:#1e40af}.lobby-contact-secondary{background:#e2e8f0;color:#1e293b}.lobby-contact-secondary:hover{background:#cbd5e1}.lobby-contact-danger{background:#fee2e2;color:#991b1b}.lobby-contact-danger:hover{background:#fecaca}.lobby-contact-toast{background:#0f172a;border-radius:10px;bottom:30px;box-shadow:0 8px 20px rgba(0,0,0,.25);color:#fff;font-size:13px;font-weight:600;left:50%;padding:10px 14px;position:fixed;transform:translateX(-50%);z-index:10030}.lobby-blocking-loader-overlay{align-items:center;background:rgba(2,6,23,.45);display:flex;inset:0;justify-content:center;padding:20px;position:fixed;z-index:10035}.lobby-blocking-loader-card{align-items:center;background:#fff;border-radius:12px;box-shadow:0 12px 30px rgba(15,23,42,.24);display:flex;gap:10px;padding:16px 18px}.lobby-blocking-loader-spinner{animation:lobby-spin .8s linear infinite;border:2px solid #cbd5e1;border-radius:50%;border-top-color:#1d4ed8;height:16px;width:16px}.lobby-blocking-loader-text{color:#0f172a;font-size:14px;font-weight:600;margin:0}.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}}@media (max-width:640px){.lobby-contact-protection-banner{align-items:flex-start;flex-direction:column}.lobby-contact-protection-btn{width:100%}.lobby-protected-leave-container{align-items:flex-start;flex-direction:column}.lobby-protected-leave-btn{width:100%}}.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:18px;margin:0!important}.share-btn svg{color:#fff;flex-shrink:0;height:18px;width:18px}@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)}.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: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:767px){.link-share-wrapper{flex-direction:column!important}}@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}";
2113
2174
  styleInject(css_248z);
2114
2175
 
2115
2176
  /// <reference lib="dom" />
2177
+ const CONTACT_PROMPT_TITLE = "Protect your group spot";
2178
+ const CONTACT_PROMPT_SUBTITLE = "Add your email or phone so we can help you recover your group membership if you leave accidentally.";
2179
+ const CONTACT_PROMPT_PRIMARY_CTA = "Protect my spot";
2180
+ const CONTACT_PROMPT_REMIND_CTA = "Remind me later";
2181
+ const CONTACT_PROMPT_INVALID_INPUT = "Please enter a valid email or phone number.";
2182
+ const CONTACT_PROMPT_SAVE_ERROR = "Could not save contact right now. Please try again.";
2183
+ const CONTACT_PROTECTED_TOAST = "Spot protected. We can help recover your group membership.";
2184
+ const LEAVE_WARNING_TITLE = "Before you leave";
2185
+ const LEAVE_WARNING_SUBTITLE = "If you leave now without adding contact, you may lose this group spot and may not be able to rejoin the same group.";
2116
2186
  /**
2117
2187
  * LobbyModal - Renders and manages the group buying lobby modal
2118
2188
  */
2119
2189
  class LobbyModal {
2120
- constructor(data, callbacks, apiClient, socketManager = null, debug = false) {
2190
+ constructor(data, callbacks, apiClient, socketManager = null, analyticsClient = null, debug = false) {
2121
2191
  this.modalElement = null;
2122
2192
  this.timerInterval = null;
2123
2193
  this.activityInterval = null;
@@ -2126,6 +2196,17 @@ var CoBuySDK = (function (exports) {
2126
2196
  this.currentGroupId = null;
2127
2197
  this.shareOverlay = null;
2128
2198
  this.keyboardHandler = null;
2199
+ this.leaveFlowInProgress = false;
2200
+ this.leaveSignalSent = false;
2201
+ this.hasContactProtection = false;
2202
+ this.initialContactPromptTimer = null;
2203
+ this.contactPromptOverlay = null;
2204
+ this.leaveLoaderOverlay = null;
2205
+ this.beforeUnloadHandler = null;
2206
+ this.pageHideHandler = null;
2207
+ this.visibilityChangeHandler = null;
2208
+ this.CONTACT_PROTECTION_PREFIX = "cobuy_contact_protection";
2209
+ this.LAST_LEFT_GROUP_PREFIX = "cobuy_last_left_group";
2129
2210
  /**
2130
2211
  * Handle socket group update events
2131
2212
  */
@@ -2217,6 +2298,7 @@ var CoBuySDK = (function (exports) {
2217
2298
  this.logger = new Logger(debug);
2218
2299
  this.apiClient = apiClient;
2219
2300
  this.socketManager = socketManager;
2301
+ this.analyticsClient = analyticsClient;
2220
2302
  // Log the group data being passed into the modal
2221
2303
  this.logger.info("LobbyModal initialized with group data", {
2222
2304
  groupId: data.groupId,
@@ -2228,14 +2310,385 @@ var CoBuySDK = (function (exports) {
2228
2310
  timeLeft: data.timeLeft,
2229
2311
  offlineRedemption: data.offlineRedemption,
2230
2312
  });
2231
- 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);
2313
+ this.data = Object.assign({ groupNumber: "1000", status: "active", progress: 80, currentMembers: 4, totalMembers: 5, timeLeft: 1390, discount: "20% OFF", isLocked: true, activities: this.getDefaultActivities(), redemptionMethod: "online" }, data);
2232
2314
  // Normalize optional flags so undefined values don't override defaults
2233
2315
  this.data.isLocked = this.computeIsLocked(this.data);
2234
2316
  if (!this.data.activities || !Array.isArray(this.data.activities)) {
2235
2317
  this.data.activities = this.getDefaultActivities();
2236
2318
  }
2319
+ this.hasContactProtection = this.readContactProtectionState();
2237
2320
  this.callbacks = callbacks;
2238
2321
  }
2322
+ getSessionId() {
2323
+ var _a;
2324
+ return ((_a = this.apiClient) === null || _a === void 0 ? void 0 : _a.getSessionId()) || "anonymous";
2325
+ }
2326
+ getContactProtectionStorageKey() {
2327
+ return `${this.CONTACT_PROTECTION_PREFIX}:${this.getSessionId()}:${this.data.productId}`;
2328
+ }
2329
+ getLastLeftGroupStorageKey() {
2330
+ return `${this.LAST_LEFT_GROUP_PREFIX}:${this.getSessionId()}:${this.data.productId}`;
2331
+ }
2332
+ readContactProtectionState() {
2333
+ if (typeof window === "undefined" || !window.localStorage) {
2334
+ return false;
2335
+ }
2336
+ try {
2337
+ return window.localStorage.getItem(this.getContactProtectionStorageKey()) === "1";
2338
+ }
2339
+ catch (_a) {
2340
+ return false;
2341
+ }
2342
+ }
2343
+ persistContactProtectionState(value) {
2344
+ this.hasContactProtection = value;
2345
+ if (typeof window === "undefined" || !window.localStorage) {
2346
+ return;
2347
+ }
2348
+ try {
2349
+ const key = this.getContactProtectionStorageKey();
2350
+ if (value) {
2351
+ window.localStorage.setItem(key, "1");
2352
+ }
2353
+ else {
2354
+ window.localStorage.removeItem(key);
2355
+ }
2356
+ }
2357
+ catch (_a) {
2358
+ // Ignore storage errors (private mode, quota exceeded, etc.)
2359
+ }
2360
+ }
2361
+ persistLastLeftGroup(groupId) {
2362
+ if (typeof window === "undefined" || !window.localStorage || !groupId) {
2363
+ return;
2364
+ }
2365
+ try {
2366
+ window.localStorage.setItem(this.getLastLeftGroupStorageKey(), JSON.stringify({
2367
+ groupId,
2368
+ productId: this.data.productId,
2369
+ leftAt: Date.now(),
2370
+ }));
2371
+ }
2372
+ catch (_a) {
2373
+ // Ignore storage errors
2374
+ }
2375
+ }
2376
+ clearInitialContactPromptTimer() {
2377
+ if (this.initialContactPromptTimer !== null) {
2378
+ window.clearTimeout(this.initialContactPromptTimer);
2379
+ this.initialContactPromptTimer = null;
2380
+ }
2381
+ }
2382
+ clearContactPromptOverlay() {
2383
+ if (this.contactPromptOverlay) {
2384
+ this.contactPromptOverlay.remove();
2385
+ this.contactPromptOverlay = null;
2386
+ }
2387
+ }
2388
+ showBlockingLoader(message) {
2389
+ if (!this.modalElement) {
2390
+ return;
2391
+ }
2392
+ this.hideBlockingLoader();
2393
+ const overlay = document.createElement("div");
2394
+ overlay.className = "lobby-blocking-loader-overlay";
2395
+ const card = document.createElement("div");
2396
+ card.className = "lobby-blocking-loader-card";
2397
+ const spinner = document.createElement("div");
2398
+ spinner.className = "lobby-blocking-loader-spinner";
2399
+ const text = document.createElement("p");
2400
+ text.className = "lobby-blocking-loader-text";
2401
+ text.textContent = message;
2402
+ card.appendChild(spinner);
2403
+ card.appendChild(text);
2404
+ overlay.appendChild(card);
2405
+ this.modalElement.appendChild(overlay);
2406
+ this.leaveLoaderOverlay = overlay;
2407
+ }
2408
+ hideBlockingLoader() {
2409
+ if (this.leaveLoaderOverlay) {
2410
+ this.leaveLoaderOverlay.remove();
2411
+ this.leaveLoaderOverlay = null;
2412
+ }
2413
+ }
2414
+ updateContactProtectionUI() {
2415
+ const banner = document.getElementById("lobbyContactProtectionBanner");
2416
+ const explicitLeaveContainer = document.getElementById("lobbyProtectedLeaveContainer");
2417
+ if (!banner)
2418
+ return;
2419
+ const canShowContactActions = this.data.status !== "complete" && Boolean(this.apiClient);
2420
+ if (!canShowContactActions) {
2421
+ banner.style.display = "none";
2422
+ if (explicitLeaveContainer) {
2423
+ explicitLeaveContainer.style.display = "none";
2424
+ }
2425
+ return;
2426
+ }
2427
+ if (this.hasContactProtection) {
2428
+ banner.style.display = "none";
2429
+ if (explicitLeaveContainer) {
2430
+ explicitLeaveContainer.style.display = "flex";
2431
+ }
2432
+ return;
2433
+ }
2434
+ banner.style.display = "flex";
2435
+ if (explicitLeaveContainer) {
2436
+ explicitLeaveContainer.style.display = "none";
2437
+ }
2438
+ }
2439
+ showToastMessage(message) {
2440
+ if (!this.modalElement) {
2441
+ return;
2442
+ }
2443
+ const existing = this.modalElement.querySelector(".lobby-contact-toast");
2444
+ if (existing) {
2445
+ existing.remove();
2446
+ }
2447
+ const toast = document.createElement("div");
2448
+ toast.className = "lobby-contact-toast";
2449
+ toast.textContent = message;
2450
+ this.modalElement.appendChild(toast);
2451
+ window.setTimeout(() => {
2452
+ toast.remove();
2453
+ }, 2400);
2454
+ }
2455
+ trackAnalyticsEvent(eventName, metadata) {
2456
+ if (!this.analyticsClient) {
2457
+ return;
2458
+ }
2459
+ this.analyticsClient
2460
+ .trackEvent(eventName, this.data.productId, Object.assign({ groupId: this.currentGroupId || this.data.groupId }, metadata))
2461
+ .catch((error) => {
2462
+ this.logger.warn(`[Analytics] Failed to track ${eventName}`, error);
2463
+ });
2464
+ }
2465
+ async saveContactValue(contactValue, promptSource) {
2466
+ const trimmed = contactValue.trim();
2467
+ if (!trimmed || !this.apiClient) {
2468
+ return false;
2469
+ }
2470
+ const contactType = this.inferContactType(trimmed);
2471
+ if (!contactType) {
2472
+ window.alert(CONTACT_PROMPT_INVALID_INPUT);
2473
+ return false;
2474
+ }
2475
+ const payload = contactType === "phone"
2476
+ ? { type: "phone", value: trimmed.replace(/\D/g, "") }
2477
+ : { type: "email", value: trimmed };
2478
+ const result = await this.apiClient.setContact(payload);
2479
+ if (!result.success) {
2480
+ this.logger.warn("Failed to save lobby contact", result.error);
2481
+ window.alert(CONTACT_PROMPT_SAVE_ERROR);
2482
+ return false;
2483
+ }
2484
+ this.persistContactProtectionState(true);
2485
+ this.trackAnalyticsEvent("CONTACT_INFO_SAVED", {
2486
+ promptSource,
2487
+ contactType,
2488
+ protectionState: "protected",
2489
+ });
2490
+ this.updateContactProtectionUI();
2491
+ this.showToastMessage(CONTACT_PROTECTED_TOAST);
2492
+ return true;
2493
+ }
2494
+ openContactProtectionPrompt(source) {
2495
+ if (typeof document === "undefined" || !this.modalElement) {
2496
+ return Promise.resolve("cancel");
2497
+ }
2498
+ this.clearContactPromptOverlay();
2499
+ this.trackAnalyticsEvent("CONTACT_PROMPT_SHOWN", {
2500
+ promptSource: source,
2501
+ protectionState: "unprotected",
2502
+ });
2503
+ return new Promise((resolve) => {
2504
+ var _a;
2505
+ const overlay = document.createElement("div");
2506
+ overlay.className = "lobby-contact-overlay";
2507
+ const card = document.createElement("div");
2508
+ card.className = "lobby-contact-card";
2509
+ const title = document.createElement("h3");
2510
+ title.className = "lobby-contact-title";
2511
+ title.textContent = CONTACT_PROMPT_TITLE;
2512
+ const subtitle = document.createElement("p");
2513
+ subtitle.className = "lobby-contact-subtitle";
2514
+ subtitle.textContent = CONTACT_PROMPT_SUBTITLE;
2515
+ const input = document.createElement("input");
2516
+ input.type = "text";
2517
+ input.className = "lobby-contact-input";
2518
+ input.placeholder = "Email or phone";
2519
+ input.autocomplete = "email";
2520
+ const actions = document.createElement("div");
2521
+ actions.className = "lobby-contact-actions";
2522
+ const primary = document.createElement("button");
2523
+ primary.type = "button";
2524
+ primary.className = "lobby-contact-primary";
2525
+ primary.textContent = CONTACT_PROMPT_PRIMARY_CTA;
2526
+ const secondary = document.createElement("button");
2527
+ secondary.type = "button";
2528
+ secondary.className = "lobby-contact-secondary";
2529
+ secondary.textContent = source === "leave-warning" ? "Cancel" : CONTACT_PROMPT_REMIND_CTA;
2530
+ const cleanupAndResolve = (result) => {
2531
+ this.clearContactPromptOverlay();
2532
+ resolve(result);
2533
+ };
2534
+ primary.addEventListener("click", async () => {
2535
+ const originalLabel = primary.textContent;
2536
+ primary.disabled = true;
2537
+ secondary.disabled = true;
2538
+ input.disabled = true;
2539
+ primary.classList.add("is-loading");
2540
+ primary.textContent = "Saving...";
2541
+ try {
2542
+ const saved = await this.saveContactValue(input.value, source);
2543
+ if (saved) {
2544
+ cleanupAndResolve("saved");
2545
+ }
2546
+ }
2547
+ finally {
2548
+ if (this.contactPromptOverlay) {
2549
+ primary.disabled = false;
2550
+ secondary.disabled = false;
2551
+ input.disabled = false;
2552
+ primary.classList.remove("is-loading");
2553
+ primary.textContent = originalLabel || CONTACT_PROMPT_PRIMARY_CTA;
2554
+ }
2555
+ }
2556
+ });
2557
+ secondary.addEventListener("click", () => {
2558
+ if (source !== "leave-warning") {
2559
+ this.trackAnalyticsEvent("CONTACT_PROMPT_DEFERRED", {
2560
+ promptSource: source,
2561
+ protectionState: "unprotected",
2562
+ });
2563
+ }
2564
+ cleanupAndResolve(source === "leave-warning" ? "cancel" : "later");
2565
+ });
2566
+ overlay.addEventListener("click", (event) => {
2567
+ if (event.target === overlay) {
2568
+ if (source !== "leave-warning") {
2569
+ this.trackAnalyticsEvent("CONTACT_PROMPT_DEFERRED", {
2570
+ promptSource: source,
2571
+ protectionState: "unprotected",
2572
+ });
2573
+ }
2574
+ cleanupAndResolve(source === "leave-warning" ? "cancel" : "later");
2575
+ }
2576
+ });
2577
+ actions.appendChild(primary);
2578
+ actions.appendChild(secondary);
2579
+ card.appendChild(title);
2580
+ card.appendChild(subtitle);
2581
+ card.appendChild(input);
2582
+ card.appendChild(actions);
2583
+ overlay.appendChild(card);
2584
+ (_a = this.modalElement) === null || _a === void 0 ? void 0 : _a.appendChild(overlay);
2585
+ this.contactPromptOverlay = overlay;
2586
+ window.setTimeout(() => input.focus(), 0);
2587
+ });
2588
+ }
2589
+ openUnprotectedLeaveWarning() {
2590
+ if (typeof document === "undefined" || !this.modalElement) {
2591
+ return Promise.resolve("cancel");
2592
+ }
2593
+ this.clearContactPromptOverlay();
2594
+ return new Promise((resolve) => {
2595
+ var _a;
2596
+ const overlay = document.createElement("div");
2597
+ overlay.className = "lobby-contact-overlay";
2598
+ const card = document.createElement("div");
2599
+ card.className = "lobby-contact-card";
2600
+ const title = document.createElement("h3");
2601
+ title.className = "lobby-contact-title";
2602
+ title.textContent = LEAVE_WARNING_TITLE;
2603
+ const subtitle = document.createElement("p");
2604
+ subtitle.className = "lobby-contact-subtitle";
2605
+ subtitle.textContent = LEAVE_WARNING_SUBTITLE;
2606
+ const actions = document.createElement("div");
2607
+ actions.className = "lobby-contact-actions";
2608
+ const addContactBtn = document.createElement("button");
2609
+ addContactBtn.type = "button";
2610
+ addContactBtn.className = "lobby-contact-primary";
2611
+ addContactBtn.textContent = "Add contact first";
2612
+ const leaveBtn = document.createElement("button");
2613
+ leaveBtn.type = "button";
2614
+ leaveBtn.className = "lobby-contact-danger";
2615
+ leaveBtn.textContent = "Leave anyway";
2616
+ const cancelBtn = document.createElement("button");
2617
+ cancelBtn.type = "button";
2618
+ cancelBtn.className = "lobby-contact-secondary";
2619
+ cancelBtn.textContent = "Cancel";
2620
+ const cleanup = (result) => {
2621
+ this.clearContactPromptOverlay();
2622
+ resolve(result);
2623
+ };
2624
+ addContactBtn.addEventListener("click", () => cleanup("add_contact"));
2625
+ leaveBtn.addEventListener("click", () => cleanup("leave"));
2626
+ cancelBtn.addEventListener("click", () => cleanup("cancel"));
2627
+ overlay.addEventListener("click", (event) => {
2628
+ if (event.target === overlay) {
2629
+ cleanup("cancel");
2630
+ }
2631
+ });
2632
+ actions.appendChild(addContactBtn);
2633
+ actions.appendChild(leaveBtn);
2634
+ actions.appendChild(cancelBtn);
2635
+ card.appendChild(title);
2636
+ card.appendChild(subtitle);
2637
+ card.appendChild(actions);
2638
+ overlay.appendChild(card);
2639
+ (_a = this.modalElement) === null || _a === void 0 ? void 0 : _a.appendChild(overlay);
2640
+ this.contactPromptOverlay = overlay;
2641
+ });
2642
+ }
2643
+ scheduleInitialContactPrompt() {
2644
+ if (!this.modalElement ||
2645
+ !this.shouldRunLeaveFlow() ||
2646
+ this.hasContactProtection ||
2647
+ !this.apiClient) {
2648
+ return;
2649
+ }
2650
+ this.clearInitialContactPromptTimer();
2651
+ this.initialContactPromptTimer = window.setTimeout(async () => {
2652
+ this.initialContactPromptTimer = null;
2653
+ if (!this.modalElement || this.hasContactProtection) {
2654
+ return;
2655
+ }
2656
+ await this.openContactProtectionPrompt("initial");
2657
+ this.updateContactProtectionUI();
2658
+ }, 2000);
2659
+ }
2660
+ async leaveGroupExplicitly() {
2661
+ var _a, _b;
2662
+ if (!this.apiClient || !this.currentGroupId || this.leaveFlowInProgress) {
2663
+ return;
2664
+ }
2665
+ this.leaveFlowInProgress = true;
2666
+ try {
2667
+ this.showBlockingLoader("Leaving group...");
2668
+ const leaveResult = await this.apiClient.leaveGroup(this.currentGroupId, {
2669
+ leave_source: "close_icon",
2670
+ leave_reason: "voluntary_with_contact",
2671
+ });
2672
+ if (!leaveResult.success) {
2673
+ this.logger.warn("Explicit leave group failed", leaveResult.error);
2674
+ this.hideBlockingLoader();
2675
+ window.alert("Could not leave the group right now. Please try again.");
2676
+ return;
2677
+ }
2678
+ this.leaveSignalSent = true;
2679
+ this.persistLastLeftGroup(((_b = (_a = leaveResult.data) === null || _a === void 0 ? void 0 : _a.group) === null || _b === void 0 ? void 0 : _b.id) || this.currentGroupId);
2680
+ this.trackAnalyticsEvent("PROTECTED_USER_EXPLICIT_LEAVE", {
2681
+ leaveSource: "close_icon",
2682
+ leaveReason: "voluntary_with_contact",
2683
+ protectionState: "protected",
2684
+ });
2685
+ this.close({ skipLeaveFlow: true });
2686
+ }
2687
+ finally {
2688
+ this.hideBlockingLoader();
2689
+ this.leaveFlowInProgress = false;
2690
+ }
2691
+ }
2239
2692
  /**
2240
2693
  * Derive lock state from data so UI reflects completion
2241
2694
  */
@@ -2552,6 +3005,7 @@ var CoBuySDK = (function (exports) {
2552
3005
  * Create connected section (subtitle + link + share)
2553
3006
  */
2554
3007
  createConnectedSection() {
3008
+ var _a, _b;
2555
3009
  const connectedSection = document.createElement("div");
2556
3010
  connectedSection.className = "connected-section";
2557
3011
  connectedSection.id = "lobbyConnectedSection";
@@ -2562,18 +3016,59 @@ var CoBuySDK = (function (exports) {
2562
3016
  subtitle.id = "lobbySubtitleText";
2563
3017
  subtitle.textContent = titleContent.subtitle;
2564
3018
  connectedSection.appendChild(subtitle);
3019
+ const contactBanner = document.createElement("div");
3020
+ contactBanner.className = "lobby-contact-protection-banner";
3021
+ contactBanner.id = "lobbyContactProtectionBanner";
3022
+ const contactCopy = document.createElement("p");
3023
+ contactCopy.className = "lobby-contact-protection-copy";
3024
+ contactCopy.textContent =
3025
+ "Spot not protected. Add contact so we can help recover your group membership.";
3026
+ const contactAction = document.createElement("button");
3027
+ contactAction.type = "button";
3028
+ contactAction.className = "lobby-contact-protection-btn";
3029
+ contactAction.textContent = "Add contact";
3030
+ contactAction.addEventListener("click", async () => {
3031
+ await this.openContactProtectionPrompt("banner");
3032
+ this.updateContactProtectionUI();
3033
+ });
3034
+ contactBanner.appendChild(contactCopy);
3035
+ contactBanner.appendChild(contactAction);
3036
+ connectedSection.appendChild(contactBanner);
3037
+ const protectedLeaveContainer = document.createElement("div");
3038
+ protectedLeaveContainer.className = "lobby-protected-leave-container";
3039
+ protectedLeaveContainer.id = "lobbyProtectedLeaveContainer";
3040
+ const protectedLeaveText = document.createElement("p");
3041
+ protectedLeaveText.className = "lobby-protected-leave-copy";
3042
+ protectedLeaveText.textContent = "You are protected and still part of this group.";
3043
+ const protectedLeaveAction = document.createElement("button");
3044
+ protectedLeaveAction.type = "button";
3045
+ protectedLeaveAction.className = "lobby-protected-leave-btn";
3046
+ protectedLeaveAction.textContent = "Leave group";
3047
+ protectedLeaveAction.addEventListener("click", () => {
3048
+ void this.leaveGroupExplicitly();
3049
+ });
3050
+ protectedLeaveContainer.appendChild(protectedLeaveText);
3051
+ protectedLeaveContainer.appendChild(protectedLeaveAction);
3052
+ connectedSection.appendChild(protectedLeaveContainer);
2565
3053
  // Check if group is fulfilled and has offline redemption
2566
3054
  const isComplete = !this.computeIsLocked(this.data);
2567
3055
  const hasOfflineRedemption = isComplete &&
2568
3056
  this.data.offlineRedemption &&
2569
- isValidOfflineRedemption(this.data.offlineRedemption);
3057
+ isValidOfflineRedemption(this.data.offlineRedemption) &&
3058
+ ((_a = this.data.redemptionMethod) !== null && _a !== void 0 ? _a : "online") !== "online";
2570
3059
  if (hasOfflineRedemption) {
2571
- // Show offline redemption view with integrated actions
3060
+ // Complete + offline/both: show offline redemption section
2572
3061
  const offlineSection = this.createOfflineRedemptionSection(this.data.offlineRedemption);
2573
3062
  connectedSection.appendChild(offlineSection);
2574
3063
  }
3064
+ else if (isComplete && ((_b = this.data.redemptionMethod) !== null && _b !== void 0 ? _b : "online") !== "offline") {
3065
+ // Complete + online/both (no offline redemption data): show only checkout CTA
3066
+ const checkoutBtn = this.createOnlineCheckoutButton();
3067
+ connectedSection.appendChild(checkoutBtn);
3068
+ this.injectOfflineRedemptionStyles();
3069
+ }
2575
3070
  else {
2576
- // Show link and share container
3071
+ // Group not yet complete: show link and share
2577
3072
  const linkShareContainer = document.createElement("div");
2578
3073
  linkShareContainer.className = "link-share-container";
2579
3074
  const linkWp = this.createLinkSection();
@@ -2617,6 +3112,23 @@ var CoBuySDK = (function (exports) {
2617
3112
  linkShareWrapper.appendChild(shareBtnInner);
2618
3113
  return linkShareWrapper;
2619
3114
  }
3115
+ /**
3116
+ * Create a standalone "Checkout Online" button for the link/share section
3117
+ */
3118
+ createOnlineCheckoutButton() {
3119
+ const btn = document.createElement("button");
3120
+ btn.className = "offline-online-checkout-btn lobby-online-checkout-btn";
3121
+ btn.textContent = "Checkout Online";
3122
+ btn.style.marginTop = "10px";
3123
+ btn.style.width = "100%";
3124
+ btn.addEventListener("click", () => {
3125
+ this.close();
3126
+ window.setTimeout(() => {
3127
+ this.triggerContinueToCheckout();
3128
+ }, 0);
3129
+ });
3130
+ return btn;
3131
+ }
2620
3132
  /**
2621
3133
  * Create offline redemption section
2622
3134
  */
@@ -2681,14 +3193,16 @@ var CoBuySDK = (function (exports) {
2681
3193
  onlineCheckoutBtn.className = "offline-online-checkout-btn";
2682
3194
  onlineCheckoutBtn.textContent = "Checkout Online";
2683
3195
  onlineCheckoutBtn.addEventListener("click", () => {
2684
- this.close();
3196
+ this.close({ skipLeaveFlow: true });
2685
3197
  // Ensure modal closes before triggering checkout intent
2686
3198
  window.setTimeout(() => {
2687
3199
  this.triggerContinueToCheckout();
2688
3200
  }, 0);
2689
3201
  });
2690
3202
  actionsRow.appendChild(downloadQRBtn);
2691
- actionsRow.appendChild(onlineCheckoutBtn);
3203
+ if (this.data.redemptionMethod !== "offline") {
3204
+ actionsRow.appendChild(onlineCheckoutBtn);
3205
+ }
2692
3206
  section.appendChild(topRow);
2693
3207
  section.appendChild(expiryInfo);
2694
3208
  section.appendChild(actionsRow);
@@ -3581,20 +4095,23 @@ var CoBuySDK = (function (exports) {
3581
4095
  document.body.appendChild(this.modalElement);
3582
4096
  // Subscribe to realtime socket events
3583
4097
  this.subscribeToSocketEvents();
3584
- // If socket is available and connected, subscribe to this group
3585
- if (this.socketManager && this.socketManager.isConnected() && this.currentGroupId) {
3586
- this.logger.info(`[LobbyModal] Socket connected, subscribing to group ${this.currentGroupId}`);
4098
+ // Subscribe to the group room for targeted events.
4099
+ // Socket.IO client buffers emits until connected, so this is safe even
4100
+ // if the handshake hasn't completed yet.
4101
+ if (this.socketManager && this.currentGroupId) {
4102
+ this.logger.info(`[LobbyModal] Subscribing to group room ${this.currentGroupId}`);
3587
4103
  this.socketManager.subscribeToGroup(this.currentGroupId);
3588
4104
  }
3589
- else if (this.socketManager && !this.socketManager.isConnected()) {
3590
- this.logger.warn("[LobbyModal] Socket manager not connected yet");
3591
- }
3592
4105
  // Start timers and animations
3593
4106
  this.startTimer();
3594
4107
  this.startActivityAnimation();
3595
4108
  this.logger.info(`Lobby modal opened for product: ${this.data.productId}`);
3596
4109
  // Set up keyboard accessibility (focus trap, ESC to close)
3597
4110
  this.setupKeyboardAccessibility();
4111
+ // Register lifecycle-based dropoff handling for browser/tab close.
4112
+ this.registerLifecycleLeaveHandlers();
4113
+ this.updateContactProtectionUI();
4114
+ this.scheduleInitialContactPrompt();
3598
4115
  });
3599
4116
  }
3600
4117
  /**
@@ -3660,19 +4177,166 @@ var CoBuySDK = (function (exports) {
3660
4177
  this.keyboardHandler = null;
3661
4178
  }
3662
4179
  }
4180
+ registerLifecycleLeaveHandlers() {
4181
+ if (typeof window === "undefined" || typeof document === "undefined") {
4182
+ return;
4183
+ }
4184
+ if (!this.shouldRunLeaveFlow()) {
4185
+ return;
4186
+ }
4187
+ if (!this.beforeUnloadHandler) {
4188
+ this.beforeUnloadHandler = () => {
4189
+ this.triggerBestEffortLeave("browser_close");
4190
+ };
4191
+ window.addEventListener("beforeunload", this.beforeUnloadHandler);
4192
+ }
4193
+ if (!this.pageHideHandler) {
4194
+ this.pageHideHandler = () => {
4195
+ this.triggerBestEffortLeave("browser_close");
4196
+ };
4197
+ window.addEventListener("pagehide", this.pageHideHandler);
4198
+ }
4199
+ if (!this.visibilityChangeHandler) {
4200
+ this.visibilityChangeHandler = () => {
4201
+ if (document.visibilityState === "hidden") {
4202
+ this.triggerBestEffortLeave("browser_close");
4203
+ }
4204
+ };
4205
+ document.addEventListener("visibilitychange", this.visibilityChangeHandler);
4206
+ }
4207
+ }
4208
+ unregisterLifecycleLeaveHandlers() {
4209
+ if (typeof window === "undefined" || typeof document === "undefined") {
4210
+ return;
4211
+ }
4212
+ if (this.beforeUnloadHandler) {
4213
+ window.removeEventListener("beforeunload", this.beforeUnloadHandler);
4214
+ this.beforeUnloadHandler = null;
4215
+ }
4216
+ if (this.pageHideHandler) {
4217
+ window.removeEventListener("pagehide", this.pageHideHandler);
4218
+ this.pageHideHandler = null;
4219
+ }
4220
+ if (this.visibilityChangeHandler) {
4221
+ document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
4222
+ this.visibilityChangeHandler = null;
4223
+ }
4224
+ }
4225
+ triggerBestEffortLeave(source) {
4226
+ if (this.leaveSignalSent || this.leaveFlowInProgress) {
4227
+ return;
4228
+ }
4229
+ if (this.hasContactProtection) {
4230
+ return;
4231
+ }
4232
+ if (!this.apiClient || !this.currentGroupId || !this.shouldRunLeaveFlow()) {
4233
+ return;
4234
+ }
4235
+ this.leaveSignalSent = true;
4236
+ this.apiClient.leaveGroupBestEffort(this.currentGroupId, {
4237
+ leave_source: source,
4238
+ leave_reason: "voluntary_no_contact",
4239
+ });
4240
+ }
4241
+ shouldRunLeaveFlow() {
4242
+ if (!this.apiClient || !this.currentGroupId) {
4243
+ return false;
4244
+ }
4245
+ // Dropoff handling applies only while group is still active/forming.
4246
+ return this.data.status !== "complete";
4247
+ }
4248
+ inferContactType(value) {
4249
+ const trimmed = value.trim();
4250
+ if (!trimmed)
4251
+ return null;
4252
+ if (trimmed.includes("@")) {
4253
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4254
+ return emailRegex.test(trimmed) ? "email" : null;
4255
+ }
4256
+ const digitsOnly = trimmed.replace(/\D/g, "");
4257
+ return digitsOnly.length >= 10 && digitsOnly.length <= 15 ? "phone" : null;
4258
+ }
4259
+ async runLeaveFlowAndClose() {
4260
+ var _a, _b;
4261
+ if (this.leaveFlowInProgress)
4262
+ return;
4263
+ this.leaveFlowInProgress = true;
4264
+ try {
4265
+ if (this.hasContactProtection) {
4266
+ this.trackAnalyticsEvent("LOBBY_CLOSED_PROTECTED", {
4267
+ leaveSource: "close_icon",
4268
+ leaveReason: "stay_in_group",
4269
+ protectionState: "protected",
4270
+ });
4271
+ this.close({ skipLeaveFlow: true });
4272
+ return;
4273
+ }
4274
+ let leaveReason = "voluntary_no_contact";
4275
+ const leaveDecision = await this.openUnprotectedLeaveWarning();
4276
+ if (leaveDecision === "cancel") {
4277
+ return;
4278
+ }
4279
+ if (leaveDecision === "add_contact") {
4280
+ const promptResult = await this.openContactProtectionPrompt("leave-warning");
4281
+ if (promptResult === "saved") {
4282
+ this.updateContactProtectionUI();
4283
+ return;
4284
+ }
4285
+ else {
4286
+ return;
4287
+ }
4288
+ }
4289
+ if (this.apiClient && this.currentGroupId) {
4290
+ this.showBlockingLoader("Leaving group...");
4291
+ const leaveResult = await this.apiClient.leaveGroup(this.currentGroupId, {
4292
+ leave_source: "close_icon",
4293
+ leave_reason: leaveReason,
4294
+ });
4295
+ if (!leaveResult.success) {
4296
+ this.logger.warn("Leave group failed", leaveResult.error);
4297
+ this.hideBlockingLoader();
4298
+ window.alert("Could not leave the group right now. Please try again.");
4299
+ return;
4300
+ }
4301
+ this.leaveSignalSent = true;
4302
+ this.persistLastLeftGroup(((_b = (_a = leaveResult.data) === null || _a === void 0 ? void 0 : _a.group) === null || _b === void 0 ? void 0 : _b.id) || this.currentGroupId);
4303
+ this.trackAnalyticsEvent("LOBBY_LEFT_UNPROTECTED", {
4304
+ leaveSource: "close_icon",
4305
+ leaveReason,
4306
+ protectionState: "unprotected",
4307
+ });
4308
+ }
4309
+ this.close({ skipLeaveFlow: true });
4310
+ }
4311
+ finally {
4312
+ this.hideBlockingLoader();
4313
+ this.leaveFlowInProgress = false;
4314
+ }
4315
+ }
3663
4316
  /**
3664
4317
  * Close the modal
3665
4318
  */
3666
- close() {
4319
+ close(options = {}) {
3667
4320
  if (!this.modalElement) {
3668
4321
  this.logger.warn("Modal not open");
3669
4322
  return;
3670
4323
  }
4324
+ if (!options.skipLeaveFlow && this.shouldRunLeaveFlow()) {
4325
+ void this.runLeaveFlowAndClose();
4326
+ return;
4327
+ }
3671
4328
  // Stop timers and animations
3672
4329
  this.stopTimer();
3673
4330
  this.stopActivityAnimation();
3674
4331
  // Remove keyboard event listeners
3675
4332
  this.removeKeyboardAccessibility();
4333
+ // Remove lifecycle handlers for dropoff detection
4334
+ this.unregisterLifecycleLeaveHandlers();
4335
+ this.clearInitialContactPromptTimer();
4336
+ this.clearContactPromptOverlay();
4337
+ this.hideBlockingLoader();
4338
+ // Remove socket listeners to avoid duplicate handlers on reopen
4339
+ this.unsubscribeFromSocketEvents();
3676
4340
  // Remove modal from DOM
3677
4341
  document.body.removeChild(this.modalElement);
3678
4342
  this.modalElement = null;
@@ -3785,29 +4449,47 @@ var CoBuySDK = (function (exports) {
3785
4449
  * Update offline redemption visibility when group is fulfilled
3786
4450
  */
3787
4451
  updateOfflineRedemptionVisibility() {
4452
+ var _a, _b;
3788
4453
  const connectedSection = document.getElementById("lobbyConnectedSection");
3789
4454
  if (!connectedSection)
3790
4455
  return;
3791
4456
  const isComplete = !this.computeIsLocked(this.data);
3792
- const hasOfflineRedemption = this.data.offlineRedemption && isValidOfflineRedemption(this.data.offlineRedemption);
4457
+ const hasOfflineRedemption = this.data.offlineRedemption &&
4458
+ isValidOfflineRedemption(this.data.offlineRedemption) &&
4459
+ ((_a = this.data.redemptionMethod) !== null && _a !== void 0 ? _a : "online") !== "online";
3793
4460
  // Get existing elements
3794
4461
  const existingOffline = connectedSection.querySelector(".offline-redemption-section");
3795
4462
  const existingLink = connectedSection.querySelector(".link-share-container");
4463
+ const existingCheckoutBtn = connectedSection.querySelector(".lobby-online-checkout-btn");
3796
4464
  if (isComplete && hasOfflineRedemption) {
3797
- // Show offline redemption, hide link/share
3798
- if (existingLink) {
4465
+ // Complete + offline/both: show offline section, remove link/share and standalone checkout btn
4466
+ if (existingLink)
3799
4467
  existingLink.remove();
3800
- }
4468
+ if (existingCheckoutBtn)
4469
+ existingCheckoutBtn.remove();
3801
4470
  if (!existingOffline) {
3802
4471
  const offlineSection = this.createOfflineRedemptionSection(this.data.offlineRedemption);
3803
4472
  connectedSection.appendChild(offlineSection);
3804
4473
  }
3805
4474
  }
3806
- else {
3807
- // Show link/share, hide offline redemption
3808
- if (existingOffline) {
4475
+ else if (isComplete && ((_b = this.data.redemptionMethod) !== null && _b !== void 0 ? _b : "online") !== "offline") {
4476
+ // Complete + online/both (no offline data): show only checkout CTA, remove link/share
4477
+ if (existingLink)
4478
+ existingLink.remove();
4479
+ if (existingOffline)
3809
4480
  existingOffline.remove();
4481
+ if (!existingCheckoutBtn) {
4482
+ const checkoutBtn = this.createOnlineCheckoutButton();
4483
+ connectedSection.appendChild(checkoutBtn);
4484
+ this.injectOfflineRedemptionStyles();
3810
4485
  }
4486
+ }
4487
+ else {
4488
+ // Group not yet complete: show link/share, remove offline and checkout btn
4489
+ if (existingOffline)
4490
+ existingOffline.remove();
4491
+ if (existingCheckoutBtn)
4492
+ existingCheckoutBtn.remove();
3811
4493
  if (!existingLink) {
3812
4494
  const linkShareContainer = document.createElement("div");
3813
4495
  linkShareContainer.className = "link-share-container";
@@ -3841,9 +4523,20 @@ var CoBuySDK = (function (exports) {
3841
4523
  }
3842
4524
  window.addEventListener("group:fulfilled", this.handleSocketGroupUpdate);
3843
4525
  window.addEventListener("group:member:joined", this.handleSocketGroupUpdate);
4526
+ window.addEventListener("group:member:left", this.handleSocketGroupUpdate);
3844
4527
  window.addEventListener("group:created", this.handleSocketGroupUpdate);
3845
4528
  this.socketListenerRegistered = true;
3846
4529
  }
4530
+ unsubscribeFromSocketEvents() {
4531
+ if (typeof window === "undefined" || !this.socketListenerRegistered) {
4532
+ return;
4533
+ }
4534
+ window.removeEventListener("group:fulfilled", this.handleSocketGroupUpdate);
4535
+ window.removeEventListener("group:member:joined", this.handleSocketGroupUpdate);
4536
+ window.removeEventListener("group:member:left", this.handleSocketGroupUpdate);
4537
+ window.removeEventListener("group:created", this.handleSocketGroupUpdate);
4538
+ this.socketListenerRegistered = false;
4539
+ }
3847
4540
  /**
3848
4541
  * Create activity item from socket event data
3849
4542
  */
@@ -3874,6 +4567,10 @@ var CoBuySDK = (function (exports) {
3874
4567
  emoji = "🛒";
3875
4568
  action = "started a new group";
3876
4569
  break;
4570
+ case "group:member:left":
4571
+ emoji = "🚪";
4572
+ action = "left the group";
4573
+ break;
3877
4574
  default:
3878
4575
  emoji = "⚡";
3879
4576
  action = "activity in group";
@@ -4346,6 +5043,9 @@ var CoBuySDK = (function (exports) {
4346
5043
  WidgetState["LOADED"] = "loaded";
4347
5044
  WidgetState["ERROR"] = "error";
4348
5045
  })(WidgetState || (WidgetState = {}));
5046
+ const RECOVERY_TOAST_REJOINED = "Rejoined your previous group.";
5047
+ const RECOVERY_TOAST_NEXT_AVAILABLE = "Your previous group was full, so we joined you to the next available group.";
5048
+ const RECOVERY_TOAST_CREATED_NEW = "Your previous group was no longer available, so we created a fresh group for you.";
4349
5049
  /**
4350
5050
  * WidgetRoot handles rendering of the CoBuy widget into a DOM container
4351
5051
  */
@@ -4377,6 +5077,7 @@ var CoBuySDK = (function (exports) {
4377
5077
  this.groupExpiryRefreshTriggered = false; // Track if expiry refresh already triggered for current group
4378
5078
  this.offlineRedemption = null;
4379
5079
  this.offlineRedemptionModal = null;
5080
+ this.campaignRedemptionMethod = "online";
4380
5081
  this.isRendering = false;
4381
5082
  this.renderPromise = null;
4382
5083
  this.liveRegionAnnouncer = null;
@@ -4386,13 +5087,22 @@ var CoBuySDK = (function (exports) {
4386
5087
  this.renderDebounceTimer = null;
4387
5088
  this.pendingRenderOptions = null;
4388
5089
  this.RENDER_DEBOUNCE_MS = 300; // 300ms debounce for rapid re-renders
5090
+ this.LAST_LEFT_GROUP_PREFIX = "cobuy_last_left_group";
4389
5091
  /** Handle backend fulfillment notifications */
4390
5092
  this.handleGroupFulfilledEvent = (event) => {
4391
5093
  const detail = event.detail;
4392
- if (!detail || !detail.productId) {
5094
+ if (!detail || !detail.product_id) {
4393
5095
  return;
4394
5096
  }
4395
- if (this.currentProductId && detail.productId !== this.currentProductId) {
5097
+ if (this.currentProductId && detail.product_id !== this.currentProductId) {
5098
+ return;
5099
+ }
5100
+ // Only enter the fulfilled/checkout flow for users who have actively joined a group this
5101
+ // session. currentSessionId is set exclusively in the setOnGroupJoined callback (when the
5102
+ // user completes a join API call). Observers who never joined should re-fetch the primary
5103
+ // group API to display the updated group state instead.
5104
+ if (!this.currentSessionId) {
5105
+ void this.refreshGroupDataFromRealtime();
4396
5106
  return;
4397
5107
  }
4398
5108
  this.processGroupFulfilled(detail);
@@ -4421,6 +5131,80 @@ var CoBuySDK = (function (exports) {
4421
5131
  this.handleCTAClick(productId);
4422
5132
  }, 300); // 300ms debounce to prevent double-clicks
4423
5133
  }
5134
+ getRecoveryStorageKey(productId) {
5135
+ var _a;
5136
+ const sessionId = (_a = this.apiClient) === null || _a === void 0 ? void 0 : _a.getSessionId();
5137
+ if (!sessionId || !productId) {
5138
+ return null;
5139
+ }
5140
+ return `${this.LAST_LEFT_GROUP_PREFIX}:${sessionId}:${productId}`;
5141
+ }
5142
+ getStoredRecoveryGroupId(productId) {
5143
+ if (typeof window === "undefined" || !window.localStorage) {
5144
+ return null;
5145
+ }
5146
+ const key = this.getRecoveryStorageKey(productId);
5147
+ if (!key) {
5148
+ return null;
5149
+ }
5150
+ try {
5151
+ const raw = window.localStorage.getItem(key);
5152
+ if (!raw) {
5153
+ return null;
5154
+ }
5155
+ const parsed = JSON.parse(raw);
5156
+ if (!(parsed === null || parsed === void 0 ? void 0 : parsed.groupId) || parsed.productId !== productId) {
5157
+ return null;
5158
+ }
5159
+ return parsed.groupId;
5160
+ }
5161
+ catch (_a) {
5162
+ return null;
5163
+ }
5164
+ }
5165
+ clearStoredRecoveryGroupId(productId) {
5166
+ if (typeof window === "undefined" || !window.localStorage) {
5167
+ return;
5168
+ }
5169
+ const key = this.getRecoveryStorageKey(productId);
5170
+ if (!key) {
5171
+ return;
5172
+ }
5173
+ try {
5174
+ window.localStorage.removeItem(key);
5175
+ }
5176
+ catch (_a) {
5177
+ // Ignore storage issues
5178
+ }
5179
+ }
5180
+ showRecoveryToast(message) {
5181
+ if (typeof document === "undefined") {
5182
+ return;
5183
+ }
5184
+ const existing = document.getElementById("cobuy-recovery-toast");
5185
+ if (existing) {
5186
+ existing.remove();
5187
+ }
5188
+ const toast = document.createElement("div");
5189
+ toast.id = "cobuy-recovery-toast";
5190
+ toast.style.position = "fixed";
5191
+ toast.style.left = "50%";
5192
+ toast.style.bottom = "24px";
5193
+ toast.style.transform = "translateX(-50%)";
5194
+ toast.style.background = "#0f172a";
5195
+ toast.style.color = "#fff";
5196
+ toast.style.padding = "10px 14px";
5197
+ toast.style.borderRadius = "10px";
5198
+ toast.style.fontSize = "13px";
5199
+ toast.style.fontWeight = "600";
5200
+ toast.style.zIndex = "10040";
5201
+ toast.style.boxShadow = "0 8px 20px rgba(0,0,0,0.24)";
5202
+ toast.textContent = message;
5203
+ document.body.appendChild(toast);
5204
+ window.setTimeout(() => {
5205
+ toast.remove();
5206
+ }, 2800);
5207
+ }
4424
5208
  /** Subscribe once to backend socket events routed through the host page */
4425
5209
  subscribeToSocketEvents() {
4426
5210
  if (typeof window === "undefined" || this.socketListenerRegistered) {
@@ -4428,6 +5212,7 @@ var CoBuySDK = (function (exports) {
4428
5212
  }
4429
5213
  window.addEventListener("group:fulfilled", this.handleGroupFulfilledEvent);
4430
5214
  window.addEventListener("group:member:joined", this.handleGroupUpdatedEvent);
5215
+ window.addEventListener("group:member:left", this.handleGroupUpdatedEvent);
4431
5216
  window.addEventListener("group:created", this.handleGroupUpdatedEvent);
4432
5217
  this.socketListenerRegistered = true;
4433
5218
  }
@@ -4449,12 +5234,24 @@ var CoBuySDK = (function (exports) {
4449
5234
  const groupData = await this.fetchPrimaryGroup(this.currentProductId);
4450
5235
  this.currentGroupData = groupData;
4451
5236
  this.currentGroupId = (groupData === null || groupData === void 0 ? void 0 : groupData.id) || this.currentGroupId;
4452
- // If backend signals completion via counts, reflect it
5237
+ // If backend signals completion via counts, reflect it — but only for actual members.
5238
+ // Observers may see a full group returned by fetchPrimaryGroup; they should NOT enter
5239
+ // the checkout flow. currentSessionId is the membership signal (non-null = joined).
4453
5240
  if (groupData) {
4454
5241
  const participants = Number(groupData.participants_count || 0);
4455
5242
  const max = Number(groupData.max_participants || 0);
4456
- if (max > 0 && participants >= max) {
5243
+ if (max > 0 && participants >= max && this.currentSessionId) {
4457
5244
  this.groupFulfilled = true;
5245
+ // The primary group API returns offline_redemption for members of fulfilled groups.
5246
+ // Extract it here so renderFulfilledSummary shows the "Redeem In-store" link
5247
+ // (matching the UI users see on a full page reload).
5248
+ if (groupData.offline_redemption &&
5249
+ isValidOfflineRedemption(groupData.offline_redemption)) {
5250
+ this.offlineRedemption = groupData.offline_redemption;
5251
+ }
5252
+ if (groupData.campaign_redemption_method) {
5253
+ this.campaignRedemptionMethod = groupData.campaign_redemption_method;
5254
+ }
4458
5255
  }
4459
5256
  }
4460
5257
  else {
@@ -4550,7 +5347,7 @@ var CoBuySDK = (function (exports) {
4550
5347
  }
4551
5348
  /** Persist fulfilled state, emit callback, and refresh UI */
4552
5349
  processGroupFulfilled(eventData) {
4553
- var _a, _b, _c, _d;
5350
+ var _a, _b, _c, _d, _e;
4554
5351
  this.groupFulfilled = true;
4555
5352
  // Extract reward from new event structure or fallback to legacy
4556
5353
  const reward = ((_a = eventData.frozen_reward) === null || _a === void 0 ? void 0 : _a.reward) || eventData.reward || null;
@@ -4565,7 +5362,10 @@ var CoBuySDK = (function (exports) {
4565
5362
  code: this.offlineRedemption.redemption_code,
4566
5363
  });
4567
5364
  }
4568
- (_d = (_c = this.events) === null || _c === void 0 ? void 0 : _c.onGroupFulfilled) === null || _d === void 0 ? void 0 : _d.call(_c, eventData);
5365
+ if ((_c = eventData.group) === null || _c === void 0 ? void 0 : _c.campaign_redemption_method) {
5366
+ this.campaignRedemptionMethod = eventData.group.campaign_redemption_method;
5367
+ }
5368
+ (_e = (_d = this.events) === null || _d === void 0 ? void 0 : _d.onGroupFulfilled) === null || _e === void 0 ? void 0 : _e.call(_d, eventData);
4569
5369
  this.renderFulfilledState();
4570
5370
  }
4571
5371
  /** Re-render widget and external containers to reflect fulfillment */
@@ -4705,8 +5505,10 @@ var CoBuySDK = (function (exports) {
4705
5505
  footer.style.justifyContent = "flex-end";
4706
5506
  footer.style.alignItems = "center";
4707
5507
  footer.style.gap = "12px";
4708
- // Add "Redeem In-store" CTA if offline redemption is available
4709
- if (this.offlineRedemption && isValidOfflineRedemption(this.offlineRedemption)) {
5508
+ // Add "Redeem In-store" CTA if offline redemption is available and campaign allows it
5509
+ if (this.offlineRedemption &&
5510
+ isValidOfflineRedemption(this.offlineRedemption) &&
5511
+ this.campaignRedemptionMethod !== "online") {
4710
5512
  const redeemLink = document.createElement("button");
4711
5513
  redeemLink.className = "cobuy-redeem-instore-link";
4712
5514
  redeemLink.style.background = "none";
@@ -4799,9 +5601,12 @@ var CoBuySDK = (function (exports) {
4799
5601
  getGroupListModal() {
4800
5602
  if (!this.groupListModal) {
4801
5603
  this.groupListModal = new GroupListModal(undefined, 8, this.config.debug, this.apiClient);
5604
+ if (this.analyticsClient) {
5605
+ this.groupListModal.setAnalyticsClient(this.analyticsClient);
5606
+ }
4802
5607
  // Set callback to open lobby when a group is successfully joined
4803
5608
  this.groupListModal.setOnGroupJoined((joinData) => {
4804
- var _a;
5609
+ var _a, _b;
4805
5610
  // Update current group ID and session ID so the modal knows which group the user has joined
4806
5611
  this.currentGroupId = joinData.group.id;
4807
5612
  this.currentSessionId = ((_a = this.apiClient) === null || _a === void 0 ? void 0 : _a.getSessionId()) || null;
@@ -4815,8 +5620,9 @@ var CoBuySDK = (function (exports) {
4815
5620
  currentMembers: joinData.group.participants_count,
4816
5621
  totalMembers: joinData.group.max_participants,
4817
5622
  timeLeft: joinData.group.timeLeftSeconds,
5623
+ redemptionMethod: (_b = joinData.group.campaign_redemption_method) !== null && _b !== void 0 ? _b : this.campaignRedemptionMethod,
4818
5624
  };
4819
- const lobbyModal = new LobbyModal(lobbyData, {}, null, null, this.config.debug);
5625
+ const lobbyModal = new LobbyModal(lobbyData, {}, this.apiClient, null, this.analyticsClient, this.config.debug);
4820
5626
  lobbyModal.open(joinData.group.id);
4821
5627
  });
4822
5628
  // Set callback for viewing progress on already joined group
@@ -4832,8 +5638,9 @@ var CoBuySDK = (function (exports) {
4832
5638
  progress: Math.round((groupData.joined / groupData.total) * 100),
4833
5639
  currentMembers: groupData.joined,
4834
5640
  totalMembers: groupData.total,
5641
+ redemptionMethod: this.campaignRedemptionMethod,
4835
5642
  };
4836
- const lobbyModal = new LobbyModal(lobbyData, {}, null, null, this.config.debug);
5643
+ const lobbyModal = new LobbyModal(lobbyData, {}, this.apiClient, null, this.analyticsClient, this.config.debug);
4837
5644
  lobbyModal.open(groupId);
4838
5645
  });
4839
5646
  }
@@ -5017,6 +5824,9 @@ var CoBuySDK = (function (exports) {
5017
5824
  isValidOfflineRedemption(groupData.offline_redemption)) {
5018
5825
  this.offlineRedemption = groupData.offline_redemption;
5019
5826
  }
5827
+ if (groupData === null || groupData === void 0 ? void 0 : groupData.campaign_redemption_method) {
5828
+ this.campaignRedemptionMethod = groupData.campaign_redemption_method;
5829
+ }
5020
5830
  // Check if group is already fulfilled (full) on initial load
5021
5831
  if (groupData) {
5022
5832
  const participants = Number(groupData.participants_count || 0);
@@ -5024,6 +5834,11 @@ var CoBuySDK = (function (exports) {
5024
5834
  if (max > 0 && participants >= max) {
5025
5835
  this.groupFulfilled = true;
5026
5836
  this.logger.info("Group is already fulfilled on initial load", { participants, max });
5837
+ if (this.analyticsClient) {
5838
+ this.analyticsClient
5839
+ .trackGroupFullView(options.productId, groupData.id, max)
5840
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
5841
+ }
5027
5842
  }
5028
5843
  }
5029
5844
  }
@@ -5089,6 +5904,11 @@ var CoBuySDK = (function (exports) {
5089
5904
  // LOADED state - render widget only if we have group data
5090
5905
  this.createWidget(rewardData, container, options);
5091
5906
  this.logger.info(`Widget rendered for product: ${options.productId}`);
5907
+ if (this.analyticsClient) {
5908
+ this.analyticsClient
5909
+ .trackCreativeView(options.productId)
5910
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
5911
+ }
5092
5912
  }
5093
5913
  else {
5094
5914
  // No group data available - hide widget
@@ -5728,10 +6548,99 @@ var CoBuySDK = (function (exports) {
5728
6548
  to { transform: rotate(360deg); }
5729
6549
  }
5730
6550
 
6551
+ .cobuy-prejoin-contact-overlay {
6552
+ position: fixed;
6553
+ inset: 0;
6554
+ z-index: 10045;
6555
+ background: rgba(2, 6, 23, 0.5);
6556
+ display: flex;
6557
+ align-items: center;
6558
+ justify-content: center;
6559
+ padding: 16px;
6560
+ }
6561
+
6562
+ .cobuy-prejoin-contact-card {
6563
+ width: min(440px, 100%);
6564
+ background: #fff;
6565
+ border-radius: 14px;
6566
+ padding: 18px;
6567
+ box-shadow: 0 14px 36px rgba(15, 23, 42, 0.22);
6568
+ display: flex;
6569
+ flex-direction: column;
6570
+ gap: 10px;
6571
+ }
6572
+
6573
+ .cobuy-prejoin-contact-title {
6574
+ font-size: 20px;
6575
+ font-weight: 800;
6576
+ color: #0f172a;
6577
+ margin: 0;
6578
+ }
6579
+
6580
+ .cobuy-prejoin-contact-subtitle {
6581
+ font-size: 14px;
6582
+ line-height: 1.5;
6583
+ color: #334155;
6584
+ margin: 0;
6585
+ }
6586
+
6587
+ .cobuy-prejoin-contact-input {
6588
+ width: 100%;
6589
+ border: 1px solid #cbd5e1;
6590
+ border-radius: 10px;
6591
+ padding: 11px 12px;
6592
+ font-size: 14px;
6593
+ outline: none;
6594
+ }
6595
+
6596
+ .cobuy-prejoin-contact-input:focus {
6597
+ border-color: #1d4ed8;
6598
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
6599
+ }
6600
+
6601
+ .cobuy-prejoin-contact-actions {
6602
+ display: flex;
6603
+ gap: 8px;
6604
+ flex-wrap: wrap;
6605
+ }
6606
+
6607
+ .cobuy-prejoin-contact-primary,
6608
+ .cobuy-prejoin-contact-secondary {
6609
+ border: none;
6610
+ border-radius: 10px;
6611
+ padding: 10px 12px;
6612
+ font-weight: 700;
6613
+ cursor: pointer;
6614
+ font-size: 13px;
6615
+ }
6616
+
6617
+ .cobuy-prejoin-contact-primary {
6618
+ background: #1d4ed8;
6619
+ color: #fff;
6620
+ }
6621
+
6622
+ .cobuy-prejoin-contact-primary:hover {
6623
+ background: #1e40af;
6624
+ }
6625
+
6626
+ .cobuy-prejoin-contact-secondary {
6627
+ background: #e2e8f0;
6628
+ color: #1e293b;
6629
+ }
6630
+
6631
+ .cobuy-prejoin-contact-secondary:hover {
6632
+ background: #cbd5e1;
6633
+ }
6634
+
5731
6635
  @media (max-width: 640px) {
5732
6636
  .cobuy-widget {
5733
6637
  grid-template-columns: 1fr;
5734
6638
  }
6639
+
6640
+ .cobuy-prejoin-contact-primary,
6641
+ .cobuy-prejoin-contact-secondary {
6642
+ width: 100%;
6643
+ }
5735
6644
  }
5736
6645
  `;
5737
6646
  document.head.appendChild(style);
@@ -5740,7 +6649,7 @@ var CoBuySDK = (function (exports) {
5740
6649
  * Handle CTA button click with analytics and modal opening
5741
6650
  */
5742
6651
  async handleCTAClick(productId) {
5743
- var _a, _b, _c, _d;
6652
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
5744
6653
  this.logger.info(`CTA clicked for product: ${productId}`);
5745
6654
  // Track analytics event asynchronously (fire-and-forget)
5746
6655
  if (this.analyticsClient) {
@@ -5755,40 +6664,157 @@ var CoBuySDK = (function (exports) {
5755
6664
  // Join group before opening modal
5756
6665
  let groupJoinData = null;
5757
6666
  let inviteData = null;
6667
+ let recoveryGroupId = null;
5758
6668
  if (this.apiClient && this.currentGroupId) {
5759
6669
  try {
5760
- this.logger.info(`Joining group: ${this.currentGroupId}`);
5761
- const joinResponse = await this.apiClient.joinGroup(this.currentGroupId);
5762
- if (!joinResponse.success) {
5763
- this.logger.error("Failed to join group, modal will not open", joinResponse.error);
5764
- this.setButtonLoadingState(false);
5765
- return; // Don't open modal on error
6670
+ recoveryGroupId = this.getStoredRecoveryGroupId(productId);
6671
+ if (this.analyticsClient && recoveryGroupId) {
6672
+ this.analyticsClient
6673
+ .trackEvent("RECOVERY_ATTEMPT", productId, {
6674
+ previousGroupId: recoveryGroupId,
6675
+ protectionState: "protected",
6676
+ })
6677
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6678
+ }
6679
+ const recoverResponse = await this.apiClient.recoverOrJoinGroup(productId);
6680
+ if (recoverResponse.success && recoverResponse.data) {
6681
+ groupJoinData = recoverResponse.data;
6682
+ this.currentGroupId = groupJoinData.group.id;
6683
+ this.clearStoredRecoveryGroupId(productId);
6684
+ if (this.analyticsClient && groupJoinData.recovery.recovered) {
6685
+ this.analyticsClient
6686
+ .trackEvent("RECOVERY_SUCCESS", productId, {
6687
+ previousGroupId: groupJoinData.recovery.previous_group_id || recoveryGroupId,
6688
+ joinedGroupId: groupJoinData.recovery.joined_group_id,
6689
+ recoveryOutcome: groupJoinData.recovery.outcome,
6690
+ protectionState: groupJoinData.recovery.matched_by === "none" ? "unprotected" : "protected",
6691
+ })
6692
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6693
+ }
6694
+ else if (this.analyticsClient && recoveryGroupId) {
6695
+ this.analyticsClient
6696
+ .trackEvent("RECOVERY_FAILED", productId, {
6697
+ previousGroupId: recoveryGroupId,
6698
+ errorCode: "NOT_RECOVERED",
6699
+ errorMessage: "Recovery did not restore the previous membership.",
6700
+ protectionState: "protected",
6701
+ })
6702
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6703
+ }
6704
+ switch (groupJoinData.recovery.outcome) {
6705
+ case "rejoined_previous_group":
6706
+ this.showRecoveryToast(RECOVERY_TOAST_REJOINED);
6707
+ break;
6708
+ case "joined_next_available_group":
6709
+ if (groupJoinData.recovery.recovered) {
6710
+ this.showRecoveryToast(RECOVERY_TOAST_NEXT_AVAILABLE);
6711
+ }
6712
+ break;
6713
+ case "created_new_group":
6714
+ if (groupJoinData.recovery.recovered) {
6715
+ this.showRecoveryToast(RECOVERY_TOAST_CREATED_NEW);
6716
+ }
6717
+ break;
6718
+ }
6719
+ }
6720
+ else {
6721
+ if (this.analyticsClient && recoveryGroupId) {
6722
+ const errCode = (_c = recoverResponse.error) === null || _c === void 0 ? void 0 : _c.code;
6723
+ const errMsg = (_d = recoverResponse.error) === null || _d === void 0 ? void 0 : _d.message;
6724
+ this.analyticsClient
6725
+ .trackEvent("RECOVERY_FAILED", productId, {
6726
+ previousGroupId: recoveryGroupId,
6727
+ errorCode: errCode || "RECOVERY_UNAVAILABLE",
6728
+ errorMessage: errMsg || "Recovery response was not successful.",
6729
+ protectionState: "protected",
6730
+ })
6731
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6732
+ }
6733
+ const joinTargetGroupId = recoveryGroupId || this.currentGroupId;
6734
+ this.logger.info(`Joining group: ${joinTargetGroupId}`);
6735
+ if (this.analyticsClient) {
6736
+ this.analyticsClient
6737
+ .trackJoinAttempt(productId, joinTargetGroupId)
6738
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6739
+ }
6740
+ let joinResponse = await this.apiClient.joinGroup(joinTargetGroupId);
6741
+ if (!joinResponse.success && recoveryGroupId && this.currentGroupId !== recoveryGroupId) {
6742
+ this.clearStoredRecoveryGroupId(productId);
6743
+ joinResponse = await this.apiClient.joinGroup(this.currentGroupId);
6744
+ }
6745
+ if (!joinResponse.success) {
6746
+ this.logger.error("Failed to join group, modal will not open", joinResponse.error);
6747
+ if (this.analyticsClient) {
6748
+ const errCode = (_e = joinResponse.error) === null || _e === void 0 ? void 0 : _e.code;
6749
+ const errMsg = (_f = joinResponse.error) === null || _f === void 0 ? void 0 : _f.message;
6750
+ this.analyticsClient
6751
+ .trackJoinFailure(productId, joinTargetGroupId, errCode, errMsg)
6752
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6753
+ }
6754
+ this.setButtonLoadingState(false);
6755
+ return; // Don't open modal on error
6756
+ }
6757
+ groupJoinData = joinResponse.data;
6758
+ if ((_g = groupJoinData === null || groupJoinData === void 0 ? void 0 : groupJoinData.group) === null || _g === void 0 ? void 0 : _g.id) {
6759
+ this.currentGroupId = groupJoinData.group.id;
6760
+ }
6761
+ if (recoveryGroupId && ((_h = groupJoinData === null || groupJoinData === void 0 ? void 0 : groupJoinData.group) === null || _h === void 0 ? void 0 : _h.id)) {
6762
+ this.clearStoredRecoveryGroupId(productId);
6763
+ if (groupJoinData.group.id !== recoveryGroupId) {
6764
+ this.showRecoveryToast(RECOVERY_TOAST_NEXT_AVAILABLE);
6765
+ }
6766
+ else {
6767
+ this.showRecoveryToast(RECOVERY_TOAST_REJOINED);
6768
+ }
6769
+ }
5766
6770
  }
5767
- groupJoinData = joinResponse.data;
5768
6771
  this.logger.info("Successfully joined group", groupJoinData);
6772
+ if (this.analyticsClient && groupJoinData) {
6773
+ this.analyticsClient
6774
+ .trackJoinSuccess(productId, groupJoinData.group.id)
6775
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6776
+ }
5769
6777
  this.offlineRedemption =
5770
6778
  groupJoinData &&
5771
6779
  groupJoinData.offline_redemption &&
5772
6780
  isValidOfflineRedemption(groupJoinData.offline_redemption)
5773
6781
  ? groupJoinData.offline_redemption
5774
6782
  : null;
6783
+ const joinedGroupId = (_j = groupJoinData === null || groupJoinData === void 0 ? void 0 : groupJoinData.group) === null || _j === void 0 ? void 0 : _j.id;
5775
6784
  // Trigger invite tracking before opening lobby (global for product)
5776
- try {
5777
- const inviteResponse = await this.apiClient.inviteToGroup(this.currentGroupId, "copy_link");
5778
- if (inviteResponse.success && inviteResponse.data) {
5779
- inviteData = inviteResponse.data;
5780
- this.logger.info("Invite link generated", inviteData);
6785
+ if (joinedGroupId) {
6786
+ try {
6787
+ const inviteResponse = await this.apiClient.inviteToGroup(joinedGroupId, "copy_link");
6788
+ if (inviteResponse.success && inviteResponse.data) {
6789
+ inviteData = inviteResponse.data;
6790
+ this.logger.info("Invite link generated", inviteData);
6791
+ }
6792
+ else {
6793
+ this.logger.warn("Invite link generation failed", inviteResponse.error);
6794
+ }
5781
6795
  }
5782
- else {
5783
- this.logger.warn("Invite link generation failed", inviteResponse.error);
6796
+ catch (inviteError) {
6797
+ this.logger.warn("Invite request failed", inviteError);
5784
6798
  }
5785
6799
  }
5786
- catch (inviteError) {
5787
- this.logger.warn("Invite request failed", inviteError);
5788
- }
5789
6800
  }
5790
6801
  catch (error) {
5791
6802
  this.logger.error("Group join failed, modal will not open", error);
6803
+ if (this.analyticsClient && recoveryGroupId) {
6804
+ this.analyticsClient
6805
+ .trackEvent("RECOVERY_FAILED", productId, {
6806
+ previousGroupId: recoveryGroupId,
6807
+ errorCode: "EXCEPTION",
6808
+ errorMessage: error instanceof Error ? error.message : String(error),
6809
+ protectionState: "protected",
6810
+ })
6811
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6812
+ }
6813
+ if (this.analyticsClient) {
6814
+ this.analyticsClient
6815
+ .trackJoinFailure(productId, (_k = this.currentGroupId) !== null && _k !== void 0 ? _k : undefined, "EXCEPTION", error instanceof Error ? error.message : String(error))
6816
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6817
+ }
5792
6818
  this.setButtonLoadingState(false);
5793
6819
  return; // Don't open modal on error
5794
6820
  }
@@ -5803,7 +6829,7 @@ var CoBuySDK = (function (exports) {
5803
6829
  const progress = Math.round((groupJoinData.group.participants_count / groupJoinData.group.max_participants) * 100);
5804
6830
  // Format discount based on reward type
5805
6831
  let discountText = "";
5806
- if ((_d = (_c = this.currentRewardData) === null || _c === void 0 ? void 0 : _c.reward) === null || _d === void 0 ? void 0 : _d.value) {
6832
+ if ((_m = (_l = this.currentRewardData) === null || _l === void 0 ? void 0 : _l.reward) === null || _m === void 0 ? void 0 : _m.value) {
5807
6833
  const rewardType = this.currentRewardData.reward.type;
5808
6834
  const rewardValue = this.currentRewardData.reward.value;
5809
6835
  if (rewardType === "percentage" || rewardType === "cashback") {
@@ -5843,6 +6869,12 @@ var CoBuySDK = (function (exports) {
5843
6869
  shareMessage: shareMessageFromInvite,
5844
6870
  isLocked: !isGroupFulfilled,
5845
6871
  offlineRedemption: offlineRedemptionFromJoin,
6872
+ redemptionMethod: (_o = groupJoinData.group.campaign_redemption_method) !== null && _o !== void 0 ? _o : this.campaignRedemptionMethod,
6873
+ onShare: this.analyticsClient
6874
+ ? () => {
6875
+ this.analyticsClient.trackShareClick(productId, groupJoinData.group.id, "other").catch((e) => this.logger.warn("Analytics tracking failed", e));
6876
+ }
6877
+ : undefined,
5846
6878
  activities: [
5847
6879
  {
5848
6880
  emoji: "👤",
@@ -5878,6 +6910,12 @@ var CoBuySDK = (function (exports) {
5878
6910
  },
5879
6911
  ], // Will be populated from real-time data later
5880
6912
  });
6913
+ // Track popup open after modal is launched
6914
+ if (this.analyticsClient) {
6915
+ this.analyticsClient
6916
+ .trackPopupOpen(productId, groupJoinData.group.id)
6917
+ .catch((e) => this.logger.warn("Analytics tracking failed", e));
6918
+ }
5881
6919
  // Remove loading state after modal opens
5882
6920
  this.setButtonLoadingState(false);
5883
6921
  }
@@ -5984,6 +7022,14 @@ var CoBuySDK = (function (exports) {
5984
7022
  getProductId() {
5985
7023
  return this.currentProductId;
5986
7024
  }
7025
+ /**
7026
+ * Returns the group ID that the current user has joined via this widget, or null if the user
7027
+ * is only an observer (has not actively joined a group this session).
7028
+ * Used by the CoBuy host class to check membership before preparing checkout.
7029
+ */
7030
+ getJoinedGroupId() {
7031
+ return this.currentSessionId ? this.currentGroupId : null;
7032
+ }
5987
7033
  }
5988
7034
 
5989
7035
  /**
@@ -5991,10 +7037,13 @@ var CoBuySDK = (function (exports) {
5991
7037
  */
5992
7038
  const API_ENDPOINTS = {
5993
7039
  // Product endpoints
7040
+ PRODUCT_CONTEXT: "/v1/sdk/products/:productId/context",
5994
7041
  PRODUCT_REWARD: "/v1/sdk/products/:productId/reward",
5995
7042
  PRODUCT_PRIMARY_GROUP: "/v1/sdk/products/:productId/group/primary",
7043
+ PRODUCT_RECOVER_OR_JOIN_GROUP: "/v1/sdk/products/:productId/group/recover-or-join",
5996
7044
  // Group endpoints
5997
7045
  GROUP_JOIN: "/v1/sdk/groups/:groupId/join",
7046
+ GROUP_LEAVE: "/v1/sdk/groups/:groupId/leave",
5998
7047
  GROUP_CREATE_AND_JOIN: "/v1/sdk/groups/new/join",
5999
7048
  PRODUCT_ACTIVE_GROUPS: "/v1/sdk/products/:productId/groups/active",
6000
7049
  GROUP_INVITE: "/v1/sdk/groups/:groupId/invite",
@@ -6041,8 +7090,11 @@ var CoBuySDK = (function (exports) {
6041
7090
  var _a;
6042
7091
  this.traceId = null;
6043
7092
  this.rewardCache = new Map();
7093
+ this.productContextCache = new Map();
6044
7094
  this.REWARD_CACHE_TTL = 60000; // 1 minute
6045
- this.pendingRequests = new Map();
7095
+ this.PRODUCT_CONTEXT_CACHE_TTL = 30000; // 30 seconds
7096
+ this.pendingRewardRequests = new Map();
7097
+ this.pendingContextRequests = new Map();
6046
7098
  this.baseUrl = config.baseUrl;
6047
7099
  this.authStrategy = config.authStrategy;
6048
7100
  this.sessionId = config.sessionId;
@@ -6438,6 +7490,106 @@ var CoBuySDK = (function (exports) {
6438
7490
  getTraceId() {
6439
7491
  return this.traceId;
6440
7492
  }
7493
+ getCachedProductContext(productId) {
7494
+ const cached = this.productContextCache.get(productId);
7495
+ if (cached && cached.expires > Date.now()) {
7496
+ return cached.data;
7497
+ }
7498
+ if (cached) {
7499
+ this.productContextCache.delete(productId);
7500
+ }
7501
+ return null;
7502
+ }
7503
+ normalizePrimaryGroup(groupData) {
7504
+ if (!groupData) {
7505
+ return null;
7506
+ }
7507
+ const nestedGroup = groupData.group;
7508
+ if (nestedGroup && typeof nestedGroup === "object" && "id" in nestedGroup) {
7509
+ return nestedGroup;
7510
+ }
7511
+ return groupData;
7512
+ }
7513
+ buildRewardDataFromContext(productId, context) {
7514
+ var _a, _b, _c, _d, _e;
7515
+ const primaryGroup = this.normalizePrimaryGroup(context.primary_group);
7516
+ return {
7517
+ productId,
7518
+ reward: (_a = context.reward) !== null && _a !== void 0 ? _a : null,
7519
+ campaign_mode: (_c = (_b = context.campaign) === null || _b === void 0 ? void 0 : _b.campaign_mode) !== null && _c !== void 0 ? _c : "group",
7520
+ campaign_creative_id: (_d = primaryGroup === null || primaryGroup === void 0 ? void 0 : primaryGroup.campaign_creative_id) !== null && _d !== void 0 ? _d : null,
7521
+ eligibility: (_e = context.eligibility) !== null && _e !== void 0 ? _e : { isEligible: false },
7522
+ };
7523
+ }
7524
+ setProductContextCache(productId, context) {
7525
+ this.productContextCache.set(productId, {
7526
+ data: context,
7527
+ expires: Date.now() + this.PRODUCT_CONTEXT_CACHE_TTL,
7528
+ });
7529
+ this.rewardCache.set(productId, {
7530
+ data: this.buildRewardDataFromContext(productId, context),
7531
+ expires: Date.now() + this.REWARD_CACHE_TTL,
7532
+ });
7533
+ }
7534
+ /**
7535
+ * Get product context information
7536
+ *
7537
+ * Uses the consolidated bootstrap endpoint so the SDK can resolve campaign,
7538
+ * reward, primary group, and active groups in a single request.
7539
+ */
7540
+ async getProductContext(productId) {
7541
+ if (!validateProductId(productId)) {
7542
+ return {
7543
+ success: false,
7544
+ error: {
7545
+ message: "Invalid productId format. Must be a non-empty string (max 200 chars).",
7546
+ code: "INVALID_PRODUCT_ID",
7547
+ },
7548
+ };
7549
+ }
7550
+ // const cached = this.getCachedProductContext(productId);
7551
+ // if (cached) {
7552
+ // this.logger.info(`Using cached product context for product: ${productId}`);
7553
+ // return {
7554
+ // success: true,
7555
+ // data: cached,
7556
+ // };
7557
+ // }
7558
+ const cacheKey = `context:${productId}`;
7559
+ const pending = this.pendingContextRequests.get(cacheKey);
7560
+ if (pending) {
7561
+ this.logger.info(`Deduplicating product context request for: ${productId}`);
7562
+ return pending;
7563
+ }
7564
+ const promise = this.fetchProductContext(productId).then((response) => {
7565
+ if (response.success && response.data) {
7566
+ this.setProductContextCache(productId, response.data);
7567
+ }
7568
+ return response;
7569
+ });
7570
+ this.pendingContextRequests.set(cacheKey, promise);
7571
+ promise.then(() => this.pendingContextRequests.delete(cacheKey), () => this.pendingContextRequests.delete(cacheKey));
7572
+ return promise;
7573
+ }
7574
+ async fetchProductContext(productId) {
7575
+ var _a;
7576
+ const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_CONTEXT, { productId });
7577
+ this.logger.info(`Fetching product context for product: ${productId}`);
7578
+ const response = await this.get(endpoint);
7579
+ if (response.success && ((_a = response.data) === null || _a === void 0 ? void 0 : _a.data)) {
7580
+ return {
7581
+ success: true,
7582
+ data: response.data.data,
7583
+ };
7584
+ }
7585
+ return {
7586
+ success: false,
7587
+ error: response.error || {
7588
+ message: "Failed to fetch product context data",
7589
+ code: "PRODUCT_CONTEXT_FETCH_ERROR",
7590
+ },
7591
+ };
7592
+ }
6441
7593
  /**
6442
7594
  * Get product reward information
6443
7595
  *
@@ -6482,13 +7634,31 @@ var CoBuySDK = (function (exports) {
6482
7634
  data: cached.data,
6483
7635
  };
6484
7636
  }
7637
+ const cachedContext = this.getCachedProductContext(productId);
7638
+ if (cachedContext) {
7639
+ this.logger.info(`Using cached product context reward for product: ${productId}`);
7640
+ return {
7641
+ success: true,
7642
+ data: this.buildRewardDataFromContext(productId, cachedContext),
7643
+ };
7644
+ }
6485
7645
  const cacheKey = `reward:${productId}`;
6486
- const pending = this.pendingRequests.get(cacheKey);
7646
+ const pending = this.pendingRewardRequests.get(cacheKey);
6487
7647
  if (pending) {
6488
7648
  this.logger.info(`Deduplicating request for product: ${productId}`);
6489
7649
  return pending;
6490
7650
  }
6491
- const promise = this.fetchProductReward(productId).then((response) => {
7651
+ const promise = (async () => {
7652
+ const contextResponse = await this.getProductContext(productId);
7653
+ if (contextResponse.success && contextResponse.data) {
7654
+ return {
7655
+ success: true,
7656
+ data: this.buildRewardDataFromContext(productId, contextResponse.data),
7657
+ };
7658
+ }
7659
+ this.logger.info(`Falling back to legacy reward endpoint for product: ${productId}`);
7660
+ return this.fetchProductReward(productId);
7661
+ })().then((response) => {
6492
7662
  if (response.success && response.data) {
6493
7663
  this.rewardCache.set(productId, {
6494
7664
  data: response.data,
@@ -6497,8 +7667,8 @@ var CoBuySDK = (function (exports) {
6497
7667
  }
6498
7668
  return response;
6499
7669
  });
6500
- this.pendingRequests.set(cacheKey, promise);
6501
- promise.then(() => this.pendingRequests.delete(cacheKey), () => this.pendingRequests.delete(cacheKey));
7670
+ this.pendingRewardRequests.set(cacheKey, promise);
7671
+ promise.then(() => this.pendingRewardRequests.delete(cacheKey), () => this.pendingRewardRequests.delete(cacheKey));
6502
7672
  return promise;
6503
7673
  }
6504
7674
  /**
@@ -6514,6 +7684,7 @@ var CoBuySDK = (function (exports) {
6514
7684
  */
6515
7685
  clearRewardCache() {
6516
7686
  this.rewardCache.clear();
7687
+ this.productContextCache.clear();
6517
7688
  }
6518
7689
  async fetchProductReward(productId) {
6519
7690
  var _a;
@@ -6561,7 +7732,7 @@ var CoBuySDK = (function (exports) {
6561
7732
  * ```
6562
7733
  */
6563
7734
  async getProductPrimaryGroup(productId) {
6564
- var _a;
7735
+ var _a, _b;
6565
7736
  if (!validateProductId(productId)) {
6566
7737
  return {
6567
7738
  success: false,
@@ -6571,15 +7742,38 @@ var CoBuySDK = (function (exports) {
6571
7742
  },
6572
7743
  };
6573
7744
  }
6574
- const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_PRIMARY_GROUP, { productId }, { allowAutoCreate: true });
6575
- this.logger.info(`Fetching primary group for product: ${productId}`);
6576
- const response = await this.get(endpoint);
6577
- if (response.success && ((_a = response.data) === null || _a === void 0 ? void 0 : _a.data)) {
7745
+ // const cachedContext = this.getCachedProductContext(productId);
7746
+ // const cachedGroup = this.normalizePrimaryGroup(cachedContext?.primary_group);
7747
+ // if (cachedGroup) {
7748
+ // this.logger.info(`Using cached primary group from product context for product: ${productId}`);
7749
+ // return {
7750
+ // success: true,
7751
+ // data: {
7752
+ // ...cachedGroup,
7753
+ // group: cachedGroup,
7754
+ // },
7755
+ // };
7756
+ // }
7757
+ const contextResponse = await this.getProductContext(productId);
7758
+ const contextGroup = this.normalizePrimaryGroup((_a = contextResponse.data) === null || _a === void 0 ? void 0 : _a.primary_group);
7759
+ if (contextResponse.success && contextGroup) {
6578
7760
  return {
6579
7761
  success: true,
6580
- data: response.data.data,
7762
+ data: Object.assign(Object.assign({}, contextGroup), { group: contextGroup }),
6581
7763
  };
6582
7764
  }
7765
+ const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_PRIMARY_GROUP, { productId }, { allowAutoCreate: true });
7766
+ this.logger.info(`Fetching primary group for product: ${productId}`);
7767
+ const response = await this.get(endpoint);
7768
+ if (response.success && ((_b = response.data) === null || _b === void 0 ? void 0 : _b.data)) {
7769
+ const normalizedGroup = this.normalizePrimaryGroup(response.data.data);
7770
+ if (normalizedGroup) {
7771
+ return {
7772
+ success: true,
7773
+ data: Object.assign(Object.assign({}, normalizedGroup), { group: normalizedGroup }),
7774
+ };
7775
+ }
7776
+ }
6583
7777
  return {
6584
7778
  success: false,
6585
7779
  error: response.error || {
@@ -6627,6 +7821,25 @@ var CoBuySDK = (function (exports) {
6627
7821
  },
6628
7822
  };
6629
7823
  }
7824
+ const cachedContext = this.getCachedProductContext(productId);
7825
+ if (cachedContext) {
7826
+ this.logger.info(`Using cached active groups from product context for product: ${productId}`);
7827
+ return {
7828
+ success: true,
7829
+ data: {
7830
+ groups: (cachedContext.active_groups || []),
7831
+ },
7832
+ };
7833
+ }
7834
+ const contextResponse = await this.getProductContext(productId);
7835
+ if (contextResponse.success && contextResponse.data) {
7836
+ return {
7837
+ success: true,
7838
+ data: {
7839
+ groups: (contextResponse.data.active_groups || []),
7840
+ },
7841
+ };
7842
+ }
6630
7843
  const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_ACTIVE_GROUPS, { productId });
6631
7844
  this.logger.info(`Fetching active groups for product: ${productId}`);
6632
7845
  const response = await this.get(endpoint);
@@ -6921,6 +8134,75 @@ var CoBuySDK = (function (exports) {
6921
8134
  },
6922
8135
  };
6923
8136
  }
8137
+ /**
8138
+ * Leave an active group for the current SDK session.
8139
+ */
8140
+ async leaveGroup(groupId, params) {
8141
+ const endpoint = buildApiUrl("", API_ENDPOINTS.GROUP_LEAVE, { groupId });
8142
+ this.logger.info(`Leaving group: ${groupId}`);
8143
+ const response = await this.post(endpoint, {
8144
+ leave_source: (params === null || params === void 0 ? void 0 : params.leave_source) || "unknown",
8145
+ leave_reason: (params === null || params === void 0 ? void 0 : params.leave_reason) || "unknown",
8146
+ });
8147
+ if (response.success) {
8148
+ const payload = response.data;
8149
+ return {
8150
+ success: true,
8151
+ data: ((payload === null || payload === void 0 ? void 0 : payload.data) || response.data),
8152
+ };
8153
+ }
8154
+ return {
8155
+ success: false,
8156
+ error: response.error || {
8157
+ message: "Failed to leave group",
8158
+ code: "LEAVE_GROUP_ERROR",
8159
+ },
8160
+ };
8161
+ }
8162
+ async recoverOrJoinGroup(productId, contact) {
8163
+ var _a;
8164
+ const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_RECOVER_OR_JOIN_GROUP, {
8165
+ productId,
8166
+ });
8167
+ this.logger.info(`Recovering or joining group for product: ${productId}`);
8168
+ const response = await this.post(endpoint, contact ? { contact } : {});
8169
+ if (response.success && ((_a = response.data) === null || _a === void 0 ? void 0 : _a.data)) {
8170
+ return {
8171
+ success: true,
8172
+ data: response.data.data,
8173
+ };
8174
+ }
8175
+ return {
8176
+ success: false,
8177
+ error: response.error || {
8178
+ message: "Failed to recover or join group",
8179
+ code: "RECOVER_OR_JOIN_GROUP_ERROR",
8180
+ },
8181
+ };
8182
+ }
8183
+ /**
8184
+ * Best-effort leave signal for unload/pagehide scenarios.
8185
+ * Uses fetch keepalive so it can run while page is closing.
8186
+ */
8187
+ leaveGroupBestEffort(groupId, params) {
8188
+ if (!groupId)
8189
+ return;
8190
+ const endpoint = buildApiUrl("", API_ENDPOINTS.GROUP_LEAVE, { groupId });
8191
+ const url = `${this.baseUrl}${endpoint}`;
8192
+ const payload = JSON.stringify({
8193
+ leave_source: (params === null || params === void 0 ? void 0 : params.leave_source) || "browser_close",
8194
+ leave_reason: (params === null || params === void 0 ? void 0 : params.leave_reason) || "voluntary_no_contact",
8195
+ });
8196
+ const authHeaders = this.authStrategy.getHeaders();
8197
+ void fetch(url, {
8198
+ method: "POST",
8199
+ headers: Object.assign({ "Content-Type": "application/json", "X-CoBuy-SDK-Version": "1.0.0", "X-CoBuy-Session": this.sessionId }, authHeaders),
8200
+ body: payload,
8201
+ keepalive: true,
8202
+ }).catch((error) => {
8203
+ this.logger.debug("Best-effort leave request failed", error);
8204
+ });
8205
+ }
6924
8206
  /**
6925
8207
  * Prepare checkout for a group
6926
8208
  *
@@ -7131,6 +8413,7 @@ var CoBuySDK = (function (exports) {
7131
8413
  const event = {
7132
8414
  event: "CTA_CLICKED",
7133
8415
  productId,
8416
+ sessionId: this.sessionId,
7134
8417
  timestamp: new Date().toISOString(),
7135
8418
  context: {
7136
8419
  pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
@@ -7183,12 +8466,32 @@ var CoBuySDK = (function (exports) {
7183
8466
  throw new CoBuyApiError(error instanceof Error ? error.message : "Unknown analytics error", "ANALYTICS_ERROR", { originalError: error });
7184
8467
  }
7185
8468
  }
8469
+ /**
8470
+ * Track session init event fired when the SDK initializes
8471
+ */
8472
+ async trackSessionInit(geo, device) {
8473
+ const event = {
8474
+ event: "SESSION_INIT",
8475
+ sessionId: this.sessionId,
8476
+ timestamp: new Date().toISOString(),
8477
+ context: Object.assign(Object.assign({ pageUrl: typeof window !== "undefined" ? window.location.href : undefined, userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined, sdkVersion: this.sdkVersion }, (geo ? { geo } : {})), (device ? { device } : {})),
8478
+ };
8479
+ try {
8480
+ await this.sendEvent(event);
8481
+ this.logger.info("[Analytics] Session init tracked");
8482
+ }
8483
+ catch (error) {
8484
+ // Non-blocking
8485
+ this.logger.error("[Analytics] Failed to track session init", error);
8486
+ }
8487
+ }
7186
8488
  /**
7187
8489
  * Track page view event
7188
8490
  */
7189
8491
  async trackPageView() {
7190
8492
  const event = {
7191
8493
  event: "PAGE_VIEW",
8494
+ sessionId: this.sessionId,
7192
8495
  timestamp: new Date().toISOString(),
7193
8496
  context: {
7194
8497
  pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
@@ -7205,6 +8508,273 @@ var CoBuySDK = (function (exports) {
7205
8508
  this.logger.error("[Analytics] Failed to track page view", error);
7206
8509
  }
7207
8510
  }
8511
+ /**
8512
+ * Track creative view event (widget rendered and visible)
8513
+ */
8514
+ async trackCreativeView(productId) {
8515
+ const event = {
8516
+ event: "CREATIVE_VIEW",
8517
+ productId,
8518
+ sessionId: this.sessionId,
8519
+ timestamp: new Date().toISOString(),
8520
+ context: {
8521
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8522
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8523
+ sdkVersion: this.sdkVersion,
8524
+ },
8525
+ };
8526
+ try {
8527
+ await this.sendEvent(event);
8528
+ this.logger.info(`[Analytics] Creative view tracked for product: ${productId}`);
8529
+ }
8530
+ catch (error) {
8531
+ this.logger.error("[Analytics] Failed to track creative view", error);
8532
+ }
8533
+ }
8534
+ /**
8535
+ * Track popup/lobby modal open event
8536
+ */
8537
+ async trackPopupOpen(productId, groupId) {
8538
+ const event = {
8539
+ event: "POPUP_OPEN",
8540
+ productId,
8541
+ sessionId: this.sessionId,
8542
+ timestamp: new Date().toISOString(),
8543
+ context: {
8544
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8545
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8546
+ sdkVersion: this.sdkVersion,
8547
+ groupId,
8548
+ },
8549
+ };
8550
+ try {
8551
+ await this.sendEvent(event);
8552
+ this.logger.info(`[Analytics] Popup open tracked for product: ${productId}`);
8553
+ }
8554
+ catch (error) {
8555
+ this.logger.error("[Analytics] Failed to track popup open", error);
8556
+ }
8557
+ }
8558
+ /**
8559
+ * Track join attempt event (before API call)
8560
+ */
8561
+ async trackJoinAttempt(productId, groupId) {
8562
+ const event = {
8563
+ event: "JOIN_ATTEMPT",
8564
+ productId,
8565
+ sessionId: this.sessionId,
8566
+ timestamp: new Date().toISOString(),
8567
+ context: {
8568
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8569
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8570
+ sdkVersion: this.sdkVersion,
8571
+ groupId,
8572
+ },
8573
+ };
8574
+ try {
8575
+ await this.sendEvent(event);
8576
+ this.logger.info(`[Analytics] Join attempt tracked for product: ${productId}`);
8577
+ }
8578
+ catch (error) {
8579
+ this.logger.error("[Analytics] Failed to track join attempt", error);
8580
+ }
8581
+ }
8582
+ /**
8583
+ * Track successful group join event
8584
+ */
8585
+ async trackJoinSuccess(productId, groupId) {
8586
+ const event = {
8587
+ event: "JOIN_SUCCESS",
8588
+ productId,
8589
+ sessionId: this.sessionId,
8590
+ timestamp: new Date().toISOString(),
8591
+ context: {
8592
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8593
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8594
+ sdkVersion: this.sdkVersion,
8595
+ groupId,
8596
+ },
8597
+ };
8598
+ try {
8599
+ await this.sendEvent(event);
8600
+ this.logger.info(`[Analytics] Join success tracked for product: ${productId}`);
8601
+ }
8602
+ catch (error) {
8603
+ this.logger.error("[Analytics] Failed to track join success", error);
8604
+ }
8605
+ }
8606
+ /**
8607
+ * Track join failure event
8608
+ */
8609
+ async trackJoinFailure(productId, groupId, errorCode, message) {
8610
+ const event = {
8611
+ event: "JOIN_FAILURE",
8612
+ productId,
8613
+ sessionId: this.sessionId,
8614
+ timestamp: new Date().toISOString(),
8615
+ context: {
8616
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8617
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8618
+ sdkVersion: this.sdkVersion,
8619
+ groupId,
8620
+ errorCode,
8621
+ errorMessage: message,
8622
+ },
8623
+ };
8624
+ try {
8625
+ await this.sendEvent(event);
8626
+ this.logger.info(`[Analytics] Join failure tracked for product: ${productId}`);
8627
+ }
8628
+ catch (error) {
8629
+ this.logger.error("[Analytics] Failed to track join failure", error);
8630
+ }
8631
+ }
8632
+ /**
8633
+ * Track already joined event (user attempts to join a group they're already in)
8634
+ */
8635
+ async trackAlreadyJoined(productId, groupId) {
8636
+ const event = {
8637
+ event: "ALREADY_JOINED",
8638
+ productId,
8639
+ sessionId: this.sessionId,
8640
+ timestamp: new Date().toISOString(),
8641
+ context: {
8642
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8643
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8644
+ sdkVersion: this.sdkVersion,
8645
+ groupId,
8646
+ },
8647
+ };
8648
+ try {
8649
+ await this.sendEvent(event);
8650
+ this.logger.info(`[Analytics] Already joined tracked for product: ${productId}`);
8651
+ }
8652
+ catch (error) {
8653
+ this.logger.error("[Analytics] Failed to track already joined", error);
8654
+ }
8655
+ }
8656
+ /**
8657
+ * Track group full view event (user sees a fulfilled/full group)
8658
+ */
8659
+ async trackGroupFullView(productId, groupId, totalMembers) {
8660
+ const event = {
8661
+ event: "GROUP_FULL_VIEW",
8662
+ productId,
8663
+ sessionId: this.sessionId,
8664
+ timestamp: new Date().toISOString(),
8665
+ context: {
8666
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8667
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8668
+ sdkVersion: this.sdkVersion,
8669
+ groupId,
8670
+ totalMembers,
8671
+ },
8672
+ };
8673
+ try {
8674
+ await this.sendEvent(event);
8675
+ this.logger.info(`[Analytics] Group full view tracked for product: ${productId}`);
8676
+ }
8677
+ catch (error) {
8678
+ this.logger.error("[Analytics] Failed to track group full view", error);
8679
+ }
8680
+ }
8681
+ /**
8682
+ * Track share click event
8683
+ */
8684
+ async trackShareClick(productId, groupId, channel) {
8685
+ const event = {
8686
+ event: "SHARE_CLICK",
8687
+ productId,
8688
+ sessionId: this.sessionId,
8689
+ timestamp: new Date().toISOString(),
8690
+ context: {
8691
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8692
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8693
+ sdkVersion: this.sdkVersion,
8694
+ groupId,
8695
+ channel,
8696
+ },
8697
+ };
8698
+ try {
8699
+ await this.sendEvent(event);
8700
+ this.logger.info(`[Analytics] Share click tracked for product: ${productId}`);
8701
+ }
8702
+ catch (error) {
8703
+ this.logger.error("[Analytics] Failed to track share click", error);
8704
+ }
8705
+ }
8706
+ /**
8707
+ * Track group creation attempt event
8708
+ */
8709
+ async trackGroupCreateAttempt(productId) {
8710
+ const event = {
8711
+ event: "GROUP_CREATE_ATTEMPT",
8712
+ productId,
8713
+ sessionId: this.sessionId,
8714
+ timestamp: new Date().toISOString(),
8715
+ context: {
8716
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8717
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8718
+ sdkVersion: this.sdkVersion,
8719
+ },
8720
+ };
8721
+ try {
8722
+ await this.sendEvent(event);
8723
+ this.logger.info(`[Analytics] Group create attempt tracked for product: ${productId}`);
8724
+ }
8725
+ catch (error) {
8726
+ this.logger.error("[Analytics] Failed to track group create attempt", error);
8727
+ }
8728
+ }
8729
+ /**
8730
+ * Track successful group creation event
8731
+ */
8732
+ async trackGroupCreateSuccess(productId, groupId) {
8733
+ const event = {
8734
+ event: "GROUP_CREATE_SUCCESS",
8735
+ productId,
8736
+ sessionId: this.sessionId,
8737
+ timestamp: new Date().toISOString(),
8738
+ context: {
8739
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8740
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8741
+ sdkVersion: this.sdkVersion,
8742
+ groupId,
8743
+ },
8744
+ };
8745
+ try {
8746
+ await this.sendEvent(event);
8747
+ this.logger.info(`[Analytics] Group create success tracked for product: ${productId}`);
8748
+ }
8749
+ catch (error) {
8750
+ this.logger.error("[Analytics] Failed to track group create success", error);
8751
+ }
8752
+ }
8753
+ /**
8754
+ * Track group creation failure event
8755
+ */
8756
+ async trackGroupCreateFailure(productId, errorCode, message) {
8757
+ const event = {
8758
+ event: "GROUP_CREATE_FAILURE",
8759
+ productId,
8760
+ sessionId: this.sessionId,
8761
+ timestamp: new Date().toISOString(),
8762
+ context: {
8763
+ pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
8764
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
8765
+ sdkVersion: this.sdkVersion,
8766
+ errorCode,
8767
+ errorMessage: message,
8768
+ },
8769
+ };
8770
+ try {
8771
+ await this.sendEvent(event);
8772
+ this.logger.info(`[Analytics] Group create failure tracked for product: ${productId}`);
8773
+ }
8774
+ catch (error) {
8775
+ this.logger.error("[Analytics] Failed to track group create failure", error);
8776
+ }
8777
+ }
7208
8778
  /**
7209
8779
  * Track custom event (extensible for future use)
7210
8780
  */
@@ -11462,15 +13032,16 @@ var CoBuySDK = (function (exports) {
11462
13032
  return;
11463
13033
  const bind = (eventName, handler) => {
11464
13034
  var _a;
11465
- if (!handler)
11466
- return;
11467
13035
  (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on(eventName, (payload) => {
11468
13036
  this.logger.info(`[Socket] ${eventName} payload: ${this.formatPayload(payload)}`);
11469
13037
  this.dispatchWindowEvent(eventName, payload);
11470
- handler(payload);
13038
+ if (handler) {
13039
+ handler(payload);
13040
+ }
11471
13041
  });
11472
13042
  };
11473
13043
  bind("group:member:joined", handlers.onGroupMemberJoined);
13044
+ bind("group:member:left", handlers.onGroupMemberLeft);
11474
13045
  bind("group:created", handlers.onGroupCreated);
11475
13046
  bind("group:fulfilled", handlers.onGroupFulfilled);
11476
13047
  }
@@ -11481,6 +13052,7 @@ var CoBuySDK = (function (exports) {
11481
13052
  if (!this.socket)
11482
13053
  return;
11483
13054
  this.socket.off("group:member:joined");
13055
+ this.socket.off("group:member:left");
11484
13056
  this.socket.off("group:created");
11485
13057
  this.socket.off("group:fulfilled");
11486
13058
  }
@@ -12650,8 +14222,14 @@ var CoBuySDK = (function (exports) {
12650
14222
  participantsCount !== undefined &&
12651
14223
  participantsCount >= maxParticipants);
12652
14224
  if (isFulfilled && productId && groupId) {
12653
- // Prepare checkout when group is fulfilled (if not already prepared)
12654
- this.prepareCheckoutIfNotDone(productId, groupId);
14225
+ // Only prepare checkout if the current user is actually a member of this group.
14226
+ // this.widgets tracks all WidgetRoot instances; getJoinedGroupId() returns the
14227
+ // joined group ID only when the user has completed a join this session (not for
14228
+ // observers). Calling prepareCheckout for non-members would generate spurious 403s.
14229
+ const userIsInGroup = [...this.widgets].some((w) => w.getJoinedGroupId() === groupId);
14230
+ if (userIsInGroup) {
14231
+ this.prepareCheckoutIfNotDone(productId, groupId);
14232
+ }
12655
14233
  }
12656
14234
  // Emit user-defined callback if provided
12657
14235
  if ((_a = config.events) === null || _a === void 0 ? void 0 : _a.onGroupMemberJoined) {
@@ -12664,8 +14242,11 @@ var CoBuySDK = (function (exports) {
12664
14242
  this.logger.warn("[SDK] Failed to initialize sockets", e);
12665
14243
  }
12666
14244
  }
12667
- // Track page view event after successful initialization
14245
+ // Track session init + page view events after successful initialization
12668
14246
  if (this.analyticsClient) {
14247
+ this.analyticsClient.trackSessionInit(config.geo, config.device).catch((error) => {
14248
+ this.logger.warn("[SDK] Failed to track session init", error);
14249
+ });
12669
14250
  this.analyticsClient.trackPageView().catch((error) => {
12670
14251
  // Non-blocking: Analytics failure should not affect SDK initialization
12671
14252
  this.logger.warn("[SDK] Failed to track page view", error);
@@ -12845,6 +14426,7 @@ var CoBuySDK = (function (exports) {
12845
14426
  activities: options.activities,
12846
14427
  isLocked: options.isLocked,
12847
14428
  offlineRedemption: options.offlineRedemption,
14429
+ redemptionMethod: options.redemptionMethod,
12848
14430
  };
12849
14431
  // Create modal instance
12850
14432
  const modal = new LobbyModal(modalData, {
@@ -12859,7 +14441,7 @@ var CoBuySDK = (function (exports) {
12859
14441
  },
12860
14442
  onCopyLink: options.onCopyLink,
12861
14443
  onShare: options.onShare,
12862
- }, this.apiClient, this.socketManager, config.debug);
14444
+ }, this.apiClient, this.socketManager, this.analyticsClient, config.debug);
12863
14445
  // Store in map for persistence
12864
14446
  this.modals.set(modalKey, modal);
12865
14447
  // Maintain backward compatibility