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