@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.
- package/dist/cobuy-sdk.esm.js +1657 -75
- package/dist/cobuy-sdk.esm.js.map +1 -1
- package/dist/cobuy-sdk.umd.js +1657 -75
- package/dist/cobuy-sdk.umd.js.map +1 -1
- package/dist/types/core/analytics.d.ts +55 -0
- package/dist/types/core/api-client.d.ts +33 -2
- package/dist/types/core/endpoints.d.ts +3 -0
- package/dist/types/core/socket.d.ts +3 -2
- package/dist/types/core/types.d.ts +118 -3
- package/dist/types/ui/group-list/group-list-modal.d.ts +4 -0
- package/dist/types/ui/lobby/lobby-modal.d.ts +49 -2
- package/dist/types/ui/widget/widget-root.d.ts +12 -0
- package/package.json +1 -1
package/dist/cobuy-sdk.esm.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
3582
|
-
|
|
3583
|
-
|
|
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 &&
|
|
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
|
-
//
|
|
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
|
-
//
|
|
3805
|
-
if (
|
|
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.
|
|
5091
|
+
if (!detail || !detail.product_id) {
|
|
4390
5092
|
return;
|
|
4391
5093
|
}
|
|
4392
|
-
if (this.currentProductId && detail.
|
|
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
|
-
|
|
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 &&
|
|
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, {},
|
|
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, {},
|
|
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
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
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
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
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
|
-
|
|
5780
|
-
this.logger.warn("Invite
|
|
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 ((
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
6498
|
-
promise.then(() => this.
|
|
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
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
12651
|
-
this.
|
|
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
|
|
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
|