@cshah18/sdk 4.12.0 → 4.14.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 +719 -26
- package/dist/cobuy-sdk.esm.js.map +1 -1
- package/dist/cobuy-sdk.umd.js +719 -26
- 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 +47 -2
- package/dist/types/core/cobuy.d.ts +27 -1
- package/dist/types/core/endpoints.d.ts +2 -0
- package/dist/types/core/types.d.ts +121 -3
- package/dist/types/ui/group-list/group-list-modal.d.ts +4 -0
- package/dist/types/ui/widget/widget-root.d.ts +6 -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,7 @@ 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);
|
|
836
840
|
const productId = detail.product_id;
|
|
837
841
|
const groupData = detail.group;
|
|
838
842
|
// Only process if this is for the current product
|
|
@@ -865,6 +869,10 @@ class GroupListModal {
|
|
|
865
869
|
this.apiClient = apiClient;
|
|
866
870
|
this.injectStyles();
|
|
867
871
|
}
|
|
872
|
+
/** Set the analytics client for event tracking */
|
|
873
|
+
setAnalyticsClient(client) {
|
|
874
|
+
this.analyticsClient = client;
|
|
875
|
+
}
|
|
868
876
|
/** Set callback for when a group is joined successfully */
|
|
869
877
|
setOnGroupJoined(callback) {
|
|
870
878
|
this.onGroupJoined = callback;
|
|
@@ -1122,10 +1130,21 @@ class GroupListModal {
|
|
|
1122
1130
|
buttonElement.disabled = true;
|
|
1123
1131
|
const originalText = button.textContent;
|
|
1124
1132
|
button.textContent = "Joining...";
|
|
1133
|
+
const productId = this.currentProductId || "";
|
|
1134
|
+
if (this.analyticsClient) {
|
|
1135
|
+
this.analyticsClient
|
|
1136
|
+
.trackJoinAttempt(productId, groupId)
|
|
1137
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
1138
|
+
}
|
|
1125
1139
|
try {
|
|
1126
1140
|
const response = await this.apiClient.joinGroup(groupId);
|
|
1127
1141
|
if (response.success && response.data) {
|
|
1128
1142
|
this.logger.info(`[handleJoinGroup] Successfully joined group: ${groupId}`, response.data);
|
|
1143
|
+
if (this.analyticsClient) {
|
|
1144
|
+
this.analyticsClient
|
|
1145
|
+
.trackJoinSuccess(productId, groupId)
|
|
1146
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
1147
|
+
}
|
|
1129
1148
|
// Track the joined group
|
|
1130
1149
|
this.currentJoinedGroupId = groupId;
|
|
1131
1150
|
// Close the modal and trigger callback with the full join response data
|
|
@@ -1136,12 +1155,22 @@ class GroupListModal {
|
|
|
1136
1155
|
}
|
|
1137
1156
|
else {
|
|
1138
1157
|
this.logger.error("Failed to join group: API response unsuccessful");
|
|
1158
|
+
if (this.analyticsClient) {
|
|
1159
|
+
this.analyticsClient
|
|
1160
|
+
.trackJoinFailure(productId, groupId)
|
|
1161
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
1162
|
+
}
|
|
1139
1163
|
button.textContent = originalText;
|
|
1140
1164
|
buttonElement.disabled = false;
|
|
1141
1165
|
}
|
|
1142
1166
|
}
|
|
1143
1167
|
catch (error) {
|
|
1144
1168
|
this.logger.error("Error joining group", error);
|
|
1169
|
+
if (this.analyticsClient) {
|
|
1170
|
+
this.analyticsClient
|
|
1171
|
+
.trackJoinFailure(productId, groupId, "EXCEPTION", error instanceof Error ? error.message : String(error))
|
|
1172
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
1173
|
+
}
|
|
1145
1174
|
button.textContent = originalText;
|
|
1146
1175
|
buttonElement.disabled = false;
|
|
1147
1176
|
}
|
|
@@ -1162,10 +1191,21 @@ class GroupListModal {
|
|
|
1162
1191
|
this.setJoinButtonsDisabled(true);
|
|
1163
1192
|
const originalText = button.textContent;
|
|
1164
1193
|
button.textContent = "Creating...";
|
|
1194
|
+
const productId = this.currentProductId;
|
|
1195
|
+
if (this.analyticsClient) {
|
|
1196
|
+
this.analyticsClient
|
|
1197
|
+
.trackGroupCreateAttempt(productId)
|
|
1198
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
1199
|
+
}
|
|
1165
1200
|
try {
|
|
1166
1201
|
const response = await this.apiClient.createAndJoinGroup(this.currentProductId);
|
|
1167
1202
|
if (response.success && response.data) {
|
|
1168
1203
|
this.logger.info("Successfully created and joined new group", response.data);
|
|
1204
|
+
if (this.analyticsClient) {
|
|
1205
|
+
this.analyticsClient
|
|
1206
|
+
.trackGroupCreateSuccess(productId, response.data.group.id)
|
|
1207
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
1208
|
+
}
|
|
1169
1209
|
this.currentJoinedGroupId = response.data.group.id;
|
|
1170
1210
|
this.close();
|
|
1171
1211
|
if (this.onGroupJoined) {
|
|
@@ -1174,6 +1214,11 @@ class GroupListModal {
|
|
|
1174
1214
|
}
|
|
1175
1215
|
else {
|
|
1176
1216
|
this.logger.error("Failed to create group: API response unsuccessful");
|
|
1217
|
+
if (this.analyticsClient) {
|
|
1218
|
+
this.analyticsClient
|
|
1219
|
+
.trackGroupCreateFailure(productId)
|
|
1220
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
1221
|
+
}
|
|
1177
1222
|
button.textContent = originalText;
|
|
1178
1223
|
buttonElement.disabled = false;
|
|
1179
1224
|
this.setJoinButtonsDisabled(false);
|
|
@@ -1181,6 +1226,11 @@ class GroupListModal {
|
|
|
1181
1226
|
}
|
|
1182
1227
|
catch (error) {
|
|
1183
1228
|
this.logger.error("Error creating group", error);
|
|
1229
|
+
if (this.analyticsClient) {
|
|
1230
|
+
this.analyticsClient
|
|
1231
|
+
.trackGroupCreateFailure(productId, "EXCEPTION", error instanceof Error ? error.message : String(error))
|
|
1232
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
1233
|
+
}
|
|
1184
1234
|
button.textContent = originalText;
|
|
1185
1235
|
buttonElement.disabled = false;
|
|
1186
1236
|
this.setJoinButtonsDisabled(false);
|
|
@@ -3578,14 +3628,13 @@ class LobbyModal {
|
|
|
3578
3628
|
document.body.appendChild(this.modalElement);
|
|
3579
3629
|
// Subscribe to realtime socket events
|
|
3580
3630
|
this.subscribeToSocketEvents();
|
|
3581
|
-
//
|
|
3582
|
-
|
|
3583
|
-
|
|
3631
|
+
// Subscribe to the group room for targeted events.
|
|
3632
|
+
// Socket.IO client buffers emits until connected, so this is safe even
|
|
3633
|
+
// if the handshake hasn't completed yet.
|
|
3634
|
+
if (this.socketManager && this.currentGroupId) {
|
|
3635
|
+
this.logger.info(`[LobbyModal] Subscribing to group room ${this.currentGroupId}`);
|
|
3584
3636
|
this.socketManager.subscribeToGroup(this.currentGroupId);
|
|
3585
3637
|
}
|
|
3586
|
-
else if (this.socketManager && !this.socketManager.isConnected()) {
|
|
3587
|
-
this.logger.warn("[LobbyModal] Socket manager not connected yet");
|
|
3588
|
-
}
|
|
3589
3638
|
// Start timers and animations
|
|
3590
3639
|
this.startTimer();
|
|
3591
3640
|
this.startActivityAnimation();
|
|
@@ -4386,10 +4435,18 @@ class WidgetRoot {
|
|
|
4386
4435
|
/** Handle backend fulfillment notifications */
|
|
4387
4436
|
this.handleGroupFulfilledEvent = (event) => {
|
|
4388
4437
|
const detail = event.detail;
|
|
4389
|
-
if (!detail || !detail.
|
|
4438
|
+
if (!detail || !detail.product_id) {
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
if (this.currentProductId && detail.product_id !== this.currentProductId) {
|
|
4390
4442
|
return;
|
|
4391
4443
|
}
|
|
4392
|
-
|
|
4444
|
+
// Only enter the fulfilled/checkout flow for users who have actively joined a group this
|
|
4445
|
+
// session. currentSessionId is set exclusively in the setOnGroupJoined callback (when the
|
|
4446
|
+
// user completes a join API call). Observers who never joined should re-fetch the primary
|
|
4447
|
+
// group API to display the updated group state instead.
|
|
4448
|
+
if (!this.currentSessionId) {
|
|
4449
|
+
void this.refreshGroupDataFromRealtime();
|
|
4393
4450
|
return;
|
|
4394
4451
|
}
|
|
4395
4452
|
this.processGroupFulfilled(detail);
|
|
@@ -4446,12 +4503,21 @@ class WidgetRoot {
|
|
|
4446
4503
|
const groupData = await this.fetchPrimaryGroup(this.currentProductId);
|
|
4447
4504
|
this.currentGroupData = groupData;
|
|
4448
4505
|
this.currentGroupId = (groupData === null || groupData === void 0 ? void 0 : groupData.id) || this.currentGroupId;
|
|
4449
|
-
// If backend signals completion via counts, reflect it
|
|
4506
|
+
// If backend signals completion via counts, reflect it — but only for actual members.
|
|
4507
|
+
// Observers may see a full group returned by fetchPrimaryGroup; they should NOT enter
|
|
4508
|
+
// the checkout flow. currentSessionId is the membership signal (non-null = joined).
|
|
4450
4509
|
if (groupData) {
|
|
4451
4510
|
const participants = Number(groupData.participants_count || 0);
|
|
4452
4511
|
const max = Number(groupData.max_participants || 0);
|
|
4453
|
-
if (max > 0 && participants >= max) {
|
|
4512
|
+
if (max > 0 && participants >= max && this.currentSessionId) {
|
|
4454
4513
|
this.groupFulfilled = true;
|
|
4514
|
+
// The primary group API returns offline_redemption for members of fulfilled groups.
|
|
4515
|
+
// Extract it here so renderFulfilledSummary shows the "Redeem In-store" link
|
|
4516
|
+
// (matching the UI users see on a full page reload).
|
|
4517
|
+
if (groupData.offline_redemption &&
|
|
4518
|
+
isValidOfflineRedemption(groupData.offline_redemption)) {
|
|
4519
|
+
this.offlineRedemption = groupData.offline_redemption;
|
|
4520
|
+
}
|
|
4455
4521
|
}
|
|
4456
4522
|
}
|
|
4457
4523
|
else {
|
|
@@ -4796,6 +4862,9 @@ class WidgetRoot {
|
|
|
4796
4862
|
getGroupListModal() {
|
|
4797
4863
|
if (!this.groupListModal) {
|
|
4798
4864
|
this.groupListModal = new GroupListModal(undefined, 8, this.config.debug, this.apiClient);
|
|
4865
|
+
if (this.analyticsClient) {
|
|
4866
|
+
this.groupListModal.setAnalyticsClient(this.analyticsClient);
|
|
4867
|
+
}
|
|
4799
4868
|
// Set callback to open lobby when a group is successfully joined
|
|
4800
4869
|
this.groupListModal.setOnGroupJoined((joinData) => {
|
|
4801
4870
|
var _a;
|
|
@@ -5021,6 +5090,11 @@ class WidgetRoot {
|
|
|
5021
5090
|
if (max > 0 && participants >= max) {
|
|
5022
5091
|
this.groupFulfilled = true;
|
|
5023
5092
|
this.logger.info("Group is already fulfilled on initial load", { participants, max });
|
|
5093
|
+
if (this.analyticsClient) {
|
|
5094
|
+
this.analyticsClient
|
|
5095
|
+
.trackGroupFullView(options.productId, groupData.id, max)
|
|
5096
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
5097
|
+
}
|
|
5024
5098
|
}
|
|
5025
5099
|
}
|
|
5026
5100
|
}
|
|
@@ -5086,6 +5160,11 @@ class WidgetRoot {
|
|
|
5086
5160
|
// LOADED state - render widget only if we have group data
|
|
5087
5161
|
this.createWidget(rewardData, container, options);
|
|
5088
5162
|
this.logger.info(`Widget rendered for product: ${options.productId}`);
|
|
5163
|
+
if (this.analyticsClient) {
|
|
5164
|
+
this.analyticsClient
|
|
5165
|
+
.trackCreativeView(options.productId)
|
|
5166
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
5167
|
+
}
|
|
5089
5168
|
}
|
|
5090
5169
|
else {
|
|
5091
5170
|
// No group data available - hide widget
|
|
@@ -5737,7 +5816,7 @@ class WidgetRoot {
|
|
|
5737
5816
|
* Handle CTA button click with analytics and modal opening
|
|
5738
5817
|
*/
|
|
5739
5818
|
async handleCTAClick(productId) {
|
|
5740
|
-
var _a, _b, _c, _d;
|
|
5819
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
5741
5820
|
this.logger.info(`CTA clicked for product: ${productId}`);
|
|
5742
5821
|
// Track analytics event asynchronously (fire-and-forget)
|
|
5743
5822
|
if (this.analyticsClient) {
|
|
@@ -5755,14 +5834,31 @@ class WidgetRoot {
|
|
|
5755
5834
|
if (this.apiClient && this.currentGroupId) {
|
|
5756
5835
|
try {
|
|
5757
5836
|
this.logger.info(`Joining group: ${this.currentGroupId}`);
|
|
5837
|
+
if (this.analyticsClient) {
|
|
5838
|
+
this.analyticsClient
|
|
5839
|
+
.trackJoinAttempt(productId, this.currentGroupId)
|
|
5840
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
5841
|
+
}
|
|
5758
5842
|
const joinResponse = await this.apiClient.joinGroup(this.currentGroupId);
|
|
5759
5843
|
if (!joinResponse.success) {
|
|
5760
5844
|
this.logger.error("Failed to join group, modal will not open", joinResponse.error);
|
|
5845
|
+
if (this.analyticsClient) {
|
|
5846
|
+
const errCode = (_c = joinResponse.error) === null || _c === void 0 ? void 0 : _c.code;
|
|
5847
|
+
const errMsg = (_d = joinResponse.error) === null || _d === void 0 ? void 0 : _d.message;
|
|
5848
|
+
this.analyticsClient
|
|
5849
|
+
.trackJoinFailure(productId, this.currentGroupId, errCode, errMsg)
|
|
5850
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
5851
|
+
}
|
|
5761
5852
|
this.setButtonLoadingState(false);
|
|
5762
5853
|
return; // Don't open modal on error
|
|
5763
5854
|
}
|
|
5764
5855
|
groupJoinData = joinResponse.data;
|
|
5765
5856
|
this.logger.info("Successfully joined group", groupJoinData);
|
|
5857
|
+
if (this.analyticsClient && groupJoinData) {
|
|
5858
|
+
this.analyticsClient
|
|
5859
|
+
.trackJoinSuccess(productId, groupJoinData.group.id)
|
|
5860
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
5861
|
+
}
|
|
5766
5862
|
this.offlineRedemption =
|
|
5767
5863
|
groupJoinData &&
|
|
5768
5864
|
groupJoinData.offline_redemption &&
|
|
@@ -5786,6 +5882,11 @@ class WidgetRoot {
|
|
|
5786
5882
|
}
|
|
5787
5883
|
catch (error) {
|
|
5788
5884
|
this.logger.error("Group join failed, modal will not open", error);
|
|
5885
|
+
if (this.analyticsClient) {
|
|
5886
|
+
this.analyticsClient
|
|
5887
|
+
.trackJoinFailure(productId, (_e = this.currentGroupId) !== null && _e !== void 0 ? _e : undefined, "EXCEPTION", error instanceof Error ? error.message : String(error))
|
|
5888
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
5889
|
+
}
|
|
5789
5890
|
this.setButtonLoadingState(false);
|
|
5790
5891
|
return; // Don't open modal on error
|
|
5791
5892
|
}
|
|
@@ -5800,7 +5901,7 @@ class WidgetRoot {
|
|
|
5800
5901
|
const progress = Math.round((groupJoinData.group.participants_count / groupJoinData.group.max_participants) * 100);
|
|
5801
5902
|
// Format discount based on reward type
|
|
5802
5903
|
let discountText = "";
|
|
5803
|
-
if ((
|
|
5904
|
+
if ((_g = (_f = this.currentRewardData) === null || _f === void 0 ? void 0 : _f.reward) === null || _g === void 0 ? void 0 : _g.value) {
|
|
5804
5905
|
const rewardType = this.currentRewardData.reward.type;
|
|
5805
5906
|
const rewardValue = this.currentRewardData.reward.value;
|
|
5806
5907
|
if (rewardType === "percentage" || rewardType === "cashback") {
|
|
@@ -5840,6 +5941,11 @@ class WidgetRoot {
|
|
|
5840
5941
|
shareMessage: shareMessageFromInvite,
|
|
5841
5942
|
isLocked: !isGroupFulfilled,
|
|
5842
5943
|
offlineRedemption: offlineRedemptionFromJoin,
|
|
5944
|
+
onShare: this.analyticsClient
|
|
5945
|
+
? () => {
|
|
5946
|
+
this.analyticsClient.trackShareClick(productId, groupJoinData.group.id, "other").catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
5947
|
+
}
|
|
5948
|
+
: undefined,
|
|
5843
5949
|
activities: [
|
|
5844
5950
|
{
|
|
5845
5951
|
emoji: "👤",
|
|
@@ -5875,6 +5981,12 @@ class WidgetRoot {
|
|
|
5875
5981
|
},
|
|
5876
5982
|
], // Will be populated from real-time data later
|
|
5877
5983
|
});
|
|
5984
|
+
// Track popup open after modal is launched
|
|
5985
|
+
if (this.analyticsClient) {
|
|
5986
|
+
this.analyticsClient
|
|
5987
|
+
.trackPopupOpen(productId, groupJoinData.group.id)
|
|
5988
|
+
.catch((e) => this.logger.warn("Analytics tracking failed", e));
|
|
5989
|
+
}
|
|
5878
5990
|
// Remove loading state after modal opens
|
|
5879
5991
|
this.setButtonLoadingState(false);
|
|
5880
5992
|
}
|
|
@@ -5981,6 +6093,14 @@ class WidgetRoot {
|
|
|
5981
6093
|
getProductId() {
|
|
5982
6094
|
return this.currentProductId;
|
|
5983
6095
|
}
|
|
6096
|
+
/**
|
|
6097
|
+
* Returns the group ID that the current user has joined via this widget, or null if the user
|
|
6098
|
+
* is only an observer (has not actively joined a group this session).
|
|
6099
|
+
* Used by the CoBuy host class to check membership before preparing checkout.
|
|
6100
|
+
*/
|
|
6101
|
+
getJoinedGroupId() {
|
|
6102
|
+
return this.currentSessionId ? this.currentGroupId : null;
|
|
6103
|
+
}
|
|
5984
6104
|
}
|
|
5985
6105
|
|
|
5986
6106
|
/**
|
|
@@ -5988,6 +6108,7 @@ class WidgetRoot {
|
|
|
5988
6108
|
*/
|
|
5989
6109
|
const API_ENDPOINTS = {
|
|
5990
6110
|
// Product endpoints
|
|
6111
|
+
PRODUCT_CONTEXT: "/v1/sdk/products/:productId/context",
|
|
5991
6112
|
PRODUCT_REWARD: "/v1/sdk/products/:productId/reward",
|
|
5992
6113
|
PRODUCT_PRIMARY_GROUP: "/v1/sdk/products/:productId/group/primary",
|
|
5993
6114
|
// Group endpoints
|
|
@@ -5998,6 +6119,8 @@ const API_ENDPOINTS = {
|
|
|
5998
6119
|
GROUP_CHECKOUT_PREPARE: "/v1/sdk/groups/:groupId/checkout/prepare",
|
|
5999
6120
|
GROUP_CHECKOUT_VALIDATE: "/v1/sdk/groups/:groupId/checkout/validate",
|
|
6000
6121
|
GROUP_CHECKOUT_CONFIRM: "/v1/sdk/groups/:groupId/checkout/confirm",
|
|
6122
|
+
// Invite endpoints
|
|
6123
|
+
INVITE_RESOLVE: "/v1/sdk/invite/resolve",
|
|
6001
6124
|
};
|
|
6002
6125
|
/**
|
|
6003
6126
|
* Build full API URL from base URL and endpoint path
|
|
@@ -6036,8 +6159,11 @@ class ApiClient {
|
|
|
6036
6159
|
var _a;
|
|
6037
6160
|
this.traceId = null;
|
|
6038
6161
|
this.rewardCache = new Map();
|
|
6162
|
+
this.productContextCache = new Map();
|
|
6039
6163
|
this.REWARD_CACHE_TTL = 60000; // 1 minute
|
|
6040
|
-
this.
|
|
6164
|
+
this.PRODUCT_CONTEXT_CACHE_TTL = 30000; // 30 seconds
|
|
6165
|
+
this.pendingRewardRequests = new Map();
|
|
6166
|
+
this.pendingContextRequests = new Map();
|
|
6041
6167
|
this.baseUrl = config.baseUrl;
|
|
6042
6168
|
this.authStrategy = config.authStrategy;
|
|
6043
6169
|
this.sessionId = config.sessionId;
|
|
@@ -6433,6 +6559,106 @@ class ApiClient {
|
|
|
6433
6559
|
getTraceId() {
|
|
6434
6560
|
return this.traceId;
|
|
6435
6561
|
}
|
|
6562
|
+
getCachedProductContext(productId) {
|
|
6563
|
+
const cached = this.productContextCache.get(productId);
|
|
6564
|
+
if (cached && cached.expires > Date.now()) {
|
|
6565
|
+
return cached.data;
|
|
6566
|
+
}
|
|
6567
|
+
if (cached) {
|
|
6568
|
+
this.productContextCache.delete(productId);
|
|
6569
|
+
}
|
|
6570
|
+
return null;
|
|
6571
|
+
}
|
|
6572
|
+
normalizePrimaryGroup(groupData) {
|
|
6573
|
+
if (!groupData) {
|
|
6574
|
+
return null;
|
|
6575
|
+
}
|
|
6576
|
+
const nestedGroup = groupData.group;
|
|
6577
|
+
if (nestedGroup && typeof nestedGroup === "object" && "id" in nestedGroup) {
|
|
6578
|
+
return nestedGroup;
|
|
6579
|
+
}
|
|
6580
|
+
return groupData;
|
|
6581
|
+
}
|
|
6582
|
+
buildRewardDataFromContext(productId, context) {
|
|
6583
|
+
var _a, _b, _c, _d, _e;
|
|
6584
|
+
const primaryGroup = this.normalizePrimaryGroup(context.primary_group);
|
|
6585
|
+
return {
|
|
6586
|
+
productId,
|
|
6587
|
+
reward: (_a = context.reward) !== null && _a !== void 0 ? _a : null,
|
|
6588
|
+
campaign_mode: (_c = (_b = context.campaign) === null || _b === void 0 ? void 0 : _b.campaign_mode) !== null && _c !== void 0 ? _c : "group",
|
|
6589
|
+
campaign_creative_id: (_d = primaryGroup === null || primaryGroup === void 0 ? void 0 : primaryGroup.campaign_creative_id) !== null && _d !== void 0 ? _d : null,
|
|
6590
|
+
eligibility: (_e = context.eligibility) !== null && _e !== void 0 ? _e : { isEligible: false },
|
|
6591
|
+
};
|
|
6592
|
+
}
|
|
6593
|
+
setProductContextCache(productId, context) {
|
|
6594
|
+
this.productContextCache.set(productId, {
|
|
6595
|
+
data: context,
|
|
6596
|
+
expires: Date.now() + this.PRODUCT_CONTEXT_CACHE_TTL,
|
|
6597
|
+
});
|
|
6598
|
+
this.rewardCache.set(productId, {
|
|
6599
|
+
data: this.buildRewardDataFromContext(productId, context),
|
|
6600
|
+
expires: Date.now() + this.REWARD_CACHE_TTL,
|
|
6601
|
+
});
|
|
6602
|
+
}
|
|
6603
|
+
/**
|
|
6604
|
+
* Get product context information
|
|
6605
|
+
*
|
|
6606
|
+
* Uses the consolidated bootstrap endpoint so the SDK can resolve campaign,
|
|
6607
|
+
* reward, primary group, and active groups in a single request.
|
|
6608
|
+
*/
|
|
6609
|
+
async getProductContext(productId) {
|
|
6610
|
+
if (!validateProductId(productId)) {
|
|
6611
|
+
return {
|
|
6612
|
+
success: false,
|
|
6613
|
+
error: {
|
|
6614
|
+
message: "Invalid productId format. Must be a non-empty string (max 200 chars).",
|
|
6615
|
+
code: "INVALID_PRODUCT_ID",
|
|
6616
|
+
},
|
|
6617
|
+
};
|
|
6618
|
+
}
|
|
6619
|
+
// const cached = this.getCachedProductContext(productId);
|
|
6620
|
+
// if (cached) {
|
|
6621
|
+
// this.logger.info(`Using cached product context for product: ${productId}`);
|
|
6622
|
+
// return {
|
|
6623
|
+
// success: true,
|
|
6624
|
+
// data: cached,
|
|
6625
|
+
// };
|
|
6626
|
+
// }
|
|
6627
|
+
const cacheKey = `context:${productId}`;
|
|
6628
|
+
const pending = this.pendingContextRequests.get(cacheKey);
|
|
6629
|
+
if (pending) {
|
|
6630
|
+
this.logger.info(`Deduplicating product context request for: ${productId}`);
|
|
6631
|
+
return pending;
|
|
6632
|
+
}
|
|
6633
|
+
const promise = this.fetchProductContext(productId).then((response) => {
|
|
6634
|
+
if (response.success && response.data) {
|
|
6635
|
+
this.setProductContextCache(productId, response.data);
|
|
6636
|
+
}
|
|
6637
|
+
return response;
|
|
6638
|
+
});
|
|
6639
|
+
this.pendingContextRequests.set(cacheKey, promise);
|
|
6640
|
+
promise.then(() => this.pendingContextRequests.delete(cacheKey), () => this.pendingContextRequests.delete(cacheKey));
|
|
6641
|
+
return promise;
|
|
6642
|
+
}
|
|
6643
|
+
async fetchProductContext(productId) {
|
|
6644
|
+
var _a;
|
|
6645
|
+
const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_CONTEXT, { productId });
|
|
6646
|
+
this.logger.info(`Fetching product context for product: ${productId}`);
|
|
6647
|
+
const response = await this.get(endpoint);
|
|
6648
|
+
if (response.success && ((_a = response.data) === null || _a === void 0 ? void 0 : _a.data)) {
|
|
6649
|
+
return {
|
|
6650
|
+
success: true,
|
|
6651
|
+
data: response.data.data,
|
|
6652
|
+
};
|
|
6653
|
+
}
|
|
6654
|
+
return {
|
|
6655
|
+
success: false,
|
|
6656
|
+
error: response.error || {
|
|
6657
|
+
message: "Failed to fetch product context data",
|
|
6658
|
+
code: "PRODUCT_CONTEXT_FETCH_ERROR",
|
|
6659
|
+
},
|
|
6660
|
+
};
|
|
6661
|
+
}
|
|
6436
6662
|
/**
|
|
6437
6663
|
* Get product reward information
|
|
6438
6664
|
*
|
|
@@ -6477,13 +6703,31 @@ class ApiClient {
|
|
|
6477
6703
|
data: cached.data,
|
|
6478
6704
|
};
|
|
6479
6705
|
}
|
|
6706
|
+
const cachedContext = this.getCachedProductContext(productId);
|
|
6707
|
+
if (cachedContext) {
|
|
6708
|
+
this.logger.info(`Using cached product context reward for product: ${productId}`);
|
|
6709
|
+
return {
|
|
6710
|
+
success: true,
|
|
6711
|
+
data: this.buildRewardDataFromContext(productId, cachedContext),
|
|
6712
|
+
};
|
|
6713
|
+
}
|
|
6480
6714
|
const cacheKey = `reward:${productId}`;
|
|
6481
|
-
const pending = this.
|
|
6715
|
+
const pending = this.pendingRewardRequests.get(cacheKey);
|
|
6482
6716
|
if (pending) {
|
|
6483
6717
|
this.logger.info(`Deduplicating request for product: ${productId}`);
|
|
6484
6718
|
return pending;
|
|
6485
6719
|
}
|
|
6486
|
-
const promise =
|
|
6720
|
+
const promise = (async () => {
|
|
6721
|
+
const contextResponse = await this.getProductContext(productId);
|
|
6722
|
+
if (contextResponse.success && contextResponse.data) {
|
|
6723
|
+
return {
|
|
6724
|
+
success: true,
|
|
6725
|
+
data: this.buildRewardDataFromContext(productId, contextResponse.data),
|
|
6726
|
+
};
|
|
6727
|
+
}
|
|
6728
|
+
this.logger.info(`Falling back to legacy reward endpoint for product: ${productId}`);
|
|
6729
|
+
return this.fetchProductReward(productId);
|
|
6730
|
+
})().then((response) => {
|
|
6487
6731
|
if (response.success && response.data) {
|
|
6488
6732
|
this.rewardCache.set(productId, {
|
|
6489
6733
|
data: response.data,
|
|
@@ -6492,8 +6736,8 @@ class ApiClient {
|
|
|
6492
6736
|
}
|
|
6493
6737
|
return response;
|
|
6494
6738
|
});
|
|
6495
|
-
this.
|
|
6496
|
-
promise.then(() => this.
|
|
6739
|
+
this.pendingRewardRequests.set(cacheKey, promise);
|
|
6740
|
+
promise.then(() => this.pendingRewardRequests.delete(cacheKey), () => this.pendingRewardRequests.delete(cacheKey));
|
|
6497
6741
|
return promise;
|
|
6498
6742
|
}
|
|
6499
6743
|
/**
|
|
@@ -6509,6 +6753,7 @@ class ApiClient {
|
|
|
6509
6753
|
*/
|
|
6510
6754
|
clearRewardCache() {
|
|
6511
6755
|
this.rewardCache.clear();
|
|
6756
|
+
this.productContextCache.clear();
|
|
6512
6757
|
}
|
|
6513
6758
|
async fetchProductReward(productId) {
|
|
6514
6759
|
var _a;
|
|
@@ -6556,7 +6801,7 @@ class ApiClient {
|
|
|
6556
6801
|
* ```
|
|
6557
6802
|
*/
|
|
6558
6803
|
async getProductPrimaryGroup(productId) {
|
|
6559
|
-
var _a;
|
|
6804
|
+
var _a, _b;
|
|
6560
6805
|
if (!validateProductId(productId)) {
|
|
6561
6806
|
return {
|
|
6562
6807
|
success: false,
|
|
@@ -6566,15 +6811,38 @@ class ApiClient {
|
|
|
6566
6811
|
},
|
|
6567
6812
|
};
|
|
6568
6813
|
}
|
|
6569
|
-
const
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6814
|
+
// const cachedContext = this.getCachedProductContext(productId);
|
|
6815
|
+
// const cachedGroup = this.normalizePrimaryGroup(cachedContext?.primary_group);
|
|
6816
|
+
// if (cachedGroup) {
|
|
6817
|
+
// this.logger.info(`Using cached primary group from product context for product: ${productId}`);
|
|
6818
|
+
// return {
|
|
6819
|
+
// success: true,
|
|
6820
|
+
// data: {
|
|
6821
|
+
// ...cachedGroup,
|
|
6822
|
+
// group: cachedGroup,
|
|
6823
|
+
// },
|
|
6824
|
+
// };
|
|
6825
|
+
// }
|
|
6826
|
+
const contextResponse = await this.getProductContext(productId);
|
|
6827
|
+
const contextGroup = this.normalizePrimaryGroup((_a = contextResponse.data) === null || _a === void 0 ? void 0 : _a.primary_group);
|
|
6828
|
+
if (contextResponse.success && contextGroup) {
|
|
6573
6829
|
return {
|
|
6574
6830
|
success: true,
|
|
6575
|
-
data:
|
|
6831
|
+
data: Object.assign(Object.assign({}, contextGroup), { group: contextGroup }),
|
|
6576
6832
|
};
|
|
6577
6833
|
}
|
|
6834
|
+
const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_PRIMARY_GROUP, { productId }, { allowAutoCreate: true });
|
|
6835
|
+
this.logger.info(`Fetching primary group for product: ${productId}`);
|
|
6836
|
+
const response = await this.get(endpoint);
|
|
6837
|
+
if (response.success && ((_b = response.data) === null || _b === void 0 ? void 0 : _b.data)) {
|
|
6838
|
+
const normalizedGroup = this.normalizePrimaryGroup(response.data.data);
|
|
6839
|
+
if (normalizedGroup) {
|
|
6840
|
+
return {
|
|
6841
|
+
success: true,
|
|
6842
|
+
data: Object.assign(Object.assign({}, normalizedGroup), { group: normalizedGroup }),
|
|
6843
|
+
};
|
|
6844
|
+
}
|
|
6845
|
+
}
|
|
6578
6846
|
return {
|
|
6579
6847
|
success: false,
|
|
6580
6848
|
error: response.error || {
|
|
@@ -6622,6 +6890,25 @@ class ApiClient {
|
|
|
6622
6890
|
},
|
|
6623
6891
|
};
|
|
6624
6892
|
}
|
|
6893
|
+
const cachedContext = this.getCachedProductContext(productId);
|
|
6894
|
+
if (cachedContext) {
|
|
6895
|
+
this.logger.info(`Using cached active groups from product context for product: ${productId}`);
|
|
6896
|
+
return {
|
|
6897
|
+
success: true,
|
|
6898
|
+
data: {
|
|
6899
|
+
groups: (cachedContext.active_groups || []),
|
|
6900
|
+
},
|
|
6901
|
+
};
|
|
6902
|
+
}
|
|
6903
|
+
const contextResponse = await this.getProductContext(productId);
|
|
6904
|
+
if (contextResponse.success && contextResponse.data) {
|
|
6905
|
+
return {
|
|
6906
|
+
success: true,
|
|
6907
|
+
data: {
|
|
6908
|
+
groups: (contextResponse.data.active_groups || []),
|
|
6909
|
+
},
|
|
6910
|
+
};
|
|
6911
|
+
}
|
|
6625
6912
|
const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_ACTIVE_GROUPS, { productId });
|
|
6626
6913
|
this.logger.info(`Fetching active groups for product: ${productId}`);
|
|
6627
6914
|
const response = await this.get(endpoint);
|
|
@@ -6808,6 +7095,58 @@ class ApiClient {
|
|
|
6808
7095
|
},
|
|
6809
7096
|
};
|
|
6810
7097
|
}
|
|
7098
|
+
/**
|
|
7099
|
+
* Resolve an invite token to get product and group information
|
|
7100
|
+
*
|
|
7101
|
+
* Resolves an invite link token (e.g., from https://brand.cobuyza.co.za/i/QMeHD7ew)
|
|
7102
|
+
* to get the associated product, group, and invite metadata.
|
|
7103
|
+
*
|
|
7104
|
+
* @param {string} inviteToken - The invite token from the URL
|
|
7105
|
+
* @returns {Promise<ApiResponse<InviteResolveResponseData>>} Product and group information
|
|
7106
|
+
*
|
|
7107
|
+
* @description
|
|
7108
|
+
* Features:
|
|
7109
|
+
* - Resolves invite tokens to product/group details
|
|
7110
|
+
* - Tracks invite click counts
|
|
7111
|
+
* - Returns invite metadata (shared_via, click_count)
|
|
7112
|
+
* - Used for invite link handling in applications
|
|
7113
|
+
*
|
|
7114
|
+
* @example
|
|
7115
|
+
* ```typescript
|
|
7116
|
+
* // Resolve invite token from URL
|
|
7117
|
+
* const response = await client.resolveInvite('QMeHD7ew');
|
|
7118
|
+
*
|
|
7119
|
+
* if (response.success) {
|
|
7120
|
+
* const productId = response.data.product.id;
|
|
7121
|
+
* const groupId = response.data.group.id;
|
|
7122
|
+
* // Navigate to product page
|
|
7123
|
+
* window.location.href = `/product/${productId}`;
|
|
7124
|
+
* }
|
|
7125
|
+
* ```
|
|
7126
|
+
*/
|
|
7127
|
+
async resolveInvite(inviteToken) {
|
|
7128
|
+
const endpoint = buildApiUrl("", API_ENDPOINTS.INVITE_RESOLVE, undefined, {
|
|
7129
|
+
invite_token: inviteToken,
|
|
7130
|
+
});
|
|
7131
|
+
this.logger.info(`Resolving invite token: ${inviteToken}`);
|
|
7132
|
+
const response = await this.get(endpoint);
|
|
7133
|
+
if (response.success) {
|
|
7134
|
+
const payload = response.data;
|
|
7135
|
+
const inviteData = ((payload === null || payload === void 0 ? void 0 : payload.data) || response.data);
|
|
7136
|
+
this.logger.info(`Invite resolved successfully for product: ${inviteData.product.id}`);
|
|
7137
|
+
return {
|
|
7138
|
+
success: true,
|
|
7139
|
+
data: inviteData,
|
|
7140
|
+
};
|
|
7141
|
+
}
|
|
7142
|
+
return {
|
|
7143
|
+
success: false,
|
|
7144
|
+
error: response.error || {
|
|
7145
|
+
message: "Failed to resolve invite",
|
|
7146
|
+
code: "INVITE_RESOLVE_ERROR",
|
|
7147
|
+
},
|
|
7148
|
+
};
|
|
7149
|
+
}
|
|
6811
7150
|
/**
|
|
6812
7151
|
* Set or update contact information for the current session
|
|
6813
7152
|
*
|
|
@@ -7074,6 +7413,7 @@ class AnalyticsClient {
|
|
|
7074
7413
|
const event = {
|
|
7075
7414
|
event: "CTA_CLICKED",
|
|
7076
7415
|
productId,
|
|
7416
|
+
sessionId: this.sessionId,
|
|
7077
7417
|
timestamp: new Date().toISOString(),
|
|
7078
7418
|
context: {
|
|
7079
7419
|
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
@@ -7126,12 +7466,32 @@ class AnalyticsClient {
|
|
|
7126
7466
|
throw new CoBuyApiError(error instanceof Error ? error.message : "Unknown analytics error", "ANALYTICS_ERROR", { originalError: error });
|
|
7127
7467
|
}
|
|
7128
7468
|
}
|
|
7469
|
+
/**
|
|
7470
|
+
* Track session init event fired when the SDK initializes
|
|
7471
|
+
*/
|
|
7472
|
+
async trackSessionInit(geo, device) {
|
|
7473
|
+
const event = {
|
|
7474
|
+
event: "SESSION_INIT",
|
|
7475
|
+
sessionId: this.sessionId,
|
|
7476
|
+
timestamp: new Date().toISOString(),
|
|
7477
|
+
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 } : {})),
|
|
7478
|
+
};
|
|
7479
|
+
try {
|
|
7480
|
+
await this.sendEvent(event);
|
|
7481
|
+
this.logger.info("[Analytics] Session init tracked");
|
|
7482
|
+
}
|
|
7483
|
+
catch (error) {
|
|
7484
|
+
// Non-blocking
|
|
7485
|
+
this.logger.error("[Analytics] Failed to track session init", error);
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7129
7488
|
/**
|
|
7130
7489
|
* Track page view event
|
|
7131
7490
|
*/
|
|
7132
7491
|
async trackPageView() {
|
|
7133
7492
|
const event = {
|
|
7134
7493
|
event: "PAGE_VIEW",
|
|
7494
|
+
sessionId: this.sessionId,
|
|
7135
7495
|
timestamp: new Date().toISOString(),
|
|
7136
7496
|
context: {
|
|
7137
7497
|
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
@@ -7148,6 +7508,273 @@ class AnalyticsClient {
|
|
|
7148
7508
|
this.logger.error("[Analytics] Failed to track page view", error);
|
|
7149
7509
|
}
|
|
7150
7510
|
}
|
|
7511
|
+
/**
|
|
7512
|
+
* Track creative view event (widget rendered and visible)
|
|
7513
|
+
*/
|
|
7514
|
+
async trackCreativeView(productId) {
|
|
7515
|
+
const event = {
|
|
7516
|
+
event: "CREATIVE_VIEW",
|
|
7517
|
+
productId,
|
|
7518
|
+
sessionId: this.sessionId,
|
|
7519
|
+
timestamp: new Date().toISOString(),
|
|
7520
|
+
context: {
|
|
7521
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7522
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7523
|
+
sdkVersion: this.sdkVersion,
|
|
7524
|
+
},
|
|
7525
|
+
};
|
|
7526
|
+
try {
|
|
7527
|
+
await this.sendEvent(event);
|
|
7528
|
+
this.logger.info(`[Analytics] Creative view tracked for product: ${productId}`);
|
|
7529
|
+
}
|
|
7530
|
+
catch (error) {
|
|
7531
|
+
this.logger.error("[Analytics] Failed to track creative view", error);
|
|
7532
|
+
}
|
|
7533
|
+
}
|
|
7534
|
+
/**
|
|
7535
|
+
* Track popup/lobby modal open event
|
|
7536
|
+
*/
|
|
7537
|
+
async trackPopupOpen(productId, groupId) {
|
|
7538
|
+
const event = {
|
|
7539
|
+
event: "POPUP_OPEN",
|
|
7540
|
+
productId,
|
|
7541
|
+
sessionId: this.sessionId,
|
|
7542
|
+
timestamp: new Date().toISOString(),
|
|
7543
|
+
context: {
|
|
7544
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7545
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7546
|
+
sdkVersion: this.sdkVersion,
|
|
7547
|
+
groupId,
|
|
7548
|
+
},
|
|
7549
|
+
};
|
|
7550
|
+
try {
|
|
7551
|
+
await this.sendEvent(event);
|
|
7552
|
+
this.logger.info(`[Analytics] Popup open tracked for product: ${productId}`);
|
|
7553
|
+
}
|
|
7554
|
+
catch (error) {
|
|
7555
|
+
this.logger.error("[Analytics] Failed to track popup open", error);
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
/**
|
|
7559
|
+
* Track join attempt event (before API call)
|
|
7560
|
+
*/
|
|
7561
|
+
async trackJoinAttempt(productId, groupId) {
|
|
7562
|
+
const event = {
|
|
7563
|
+
event: "JOIN_ATTEMPT",
|
|
7564
|
+
productId,
|
|
7565
|
+
sessionId: this.sessionId,
|
|
7566
|
+
timestamp: new Date().toISOString(),
|
|
7567
|
+
context: {
|
|
7568
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7569
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7570
|
+
sdkVersion: this.sdkVersion,
|
|
7571
|
+
groupId,
|
|
7572
|
+
},
|
|
7573
|
+
};
|
|
7574
|
+
try {
|
|
7575
|
+
await this.sendEvent(event);
|
|
7576
|
+
this.logger.info(`[Analytics] Join attempt tracked for product: ${productId}`);
|
|
7577
|
+
}
|
|
7578
|
+
catch (error) {
|
|
7579
|
+
this.logger.error("[Analytics] Failed to track join attempt", error);
|
|
7580
|
+
}
|
|
7581
|
+
}
|
|
7582
|
+
/**
|
|
7583
|
+
* Track successful group join event
|
|
7584
|
+
*/
|
|
7585
|
+
async trackJoinSuccess(productId, groupId) {
|
|
7586
|
+
const event = {
|
|
7587
|
+
event: "JOIN_SUCCESS",
|
|
7588
|
+
productId,
|
|
7589
|
+
sessionId: this.sessionId,
|
|
7590
|
+
timestamp: new Date().toISOString(),
|
|
7591
|
+
context: {
|
|
7592
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7593
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7594
|
+
sdkVersion: this.sdkVersion,
|
|
7595
|
+
groupId,
|
|
7596
|
+
},
|
|
7597
|
+
};
|
|
7598
|
+
try {
|
|
7599
|
+
await this.sendEvent(event);
|
|
7600
|
+
this.logger.info(`[Analytics] Join success tracked for product: ${productId}`);
|
|
7601
|
+
}
|
|
7602
|
+
catch (error) {
|
|
7603
|
+
this.logger.error("[Analytics] Failed to track join success", error);
|
|
7604
|
+
}
|
|
7605
|
+
}
|
|
7606
|
+
/**
|
|
7607
|
+
* Track join failure event
|
|
7608
|
+
*/
|
|
7609
|
+
async trackJoinFailure(productId, groupId, errorCode, message) {
|
|
7610
|
+
const event = {
|
|
7611
|
+
event: "JOIN_FAILURE",
|
|
7612
|
+
productId,
|
|
7613
|
+
sessionId: this.sessionId,
|
|
7614
|
+
timestamp: new Date().toISOString(),
|
|
7615
|
+
context: {
|
|
7616
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7617
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7618
|
+
sdkVersion: this.sdkVersion,
|
|
7619
|
+
groupId,
|
|
7620
|
+
errorCode,
|
|
7621
|
+
errorMessage: message,
|
|
7622
|
+
},
|
|
7623
|
+
};
|
|
7624
|
+
try {
|
|
7625
|
+
await this.sendEvent(event);
|
|
7626
|
+
this.logger.info(`[Analytics] Join failure tracked for product: ${productId}`);
|
|
7627
|
+
}
|
|
7628
|
+
catch (error) {
|
|
7629
|
+
this.logger.error("[Analytics] Failed to track join failure", error);
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
/**
|
|
7633
|
+
* Track already joined event (user attempts to join a group they're already in)
|
|
7634
|
+
*/
|
|
7635
|
+
async trackAlreadyJoined(productId, groupId) {
|
|
7636
|
+
const event = {
|
|
7637
|
+
event: "ALREADY_JOINED",
|
|
7638
|
+
productId,
|
|
7639
|
+
sessionId: this.sessionId,
|
|
7640
|
+
timestamp: new Date().toISOString(),
|
|
7641
|
+
context: {
|
|
7642
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7643
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7644
|
+
sdkVersion: this.sdkVersion,
|
|
7645
|
+
groupId,
|
|
7646
|
+
},
|
|
7647
|
+
};
|
|
7648
|
+
try {
|
|
7649
|
+
await this.sendEvent(event);
|
|
7650
|
+
this.logger.info(`[Analytics] Already joined tracked for product: ${productId}`);
|
|
7651
|
+
}
|
|
7652
|
+
catch (error) {
|
|
7653
|
+
this.logger.error("[Analytics] Failed to track already joined", error);
|
|
7654
|
+
}
|
|
7655
|
+
}
|
|
7656
|
+
/**
|
|
7657
|
+
* Track group full view event (user sees a fulfilled/full group)
|
|
7658
|
+
*/
|
|
7659
|
+
async trackGroupFullView(productId, groupId, totalMembers) {
|
|
7660
|
+
const event = {
|
|
7661
|
+
event: "GROUP_FULL_VIEW",
|
|
7662
|
+
productId,
|
|
7663
|
+
sessionId: this.sessionId,
|
|
7664
|
+
timestamp: new Date().toISOString(),
|
|
7665
|
+
context: {
|
|
7666
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7667
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7668
|
+
sdkVersion: this.sdkVersion,
|
|
7669
|
+
groupId,
|
|
7670
|
+
totalMembers,
|
|
7671
|
+
},
|
|
7672
|
+
};
|
|
7673
|
+
try {
|
|
7674
|
+
await this.sendEvent(event);
|
|
7675
|
+
this.logger.info(`[Analytics] Group full view tracked for product: ${productId}`);
|
|
7676
|
+
}
|
|
7677
|
+
catch (error) {
|
|
7678
|
+
this.logger.error("[Analytics] Failed to track group full view", error);
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
/**
|
|
7682
|
+
* Track share click event
|
|
7683
|
+
*/
|
|
7684
|
+
async trackShareClick(productId, groupId, channel) {
|
|
7685
|
+
const event = {
|
|
7686
|
+
event: "SHARE_CLICK",
|
|
7687
|
+
productId,
|
|
7688
|
+
sessionId: this.sessionId,
|
|
7689
|
+
timestamp: new Date().toISOString(),
|
|
7690
|
+
context: {
|
|
7691
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7692
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7693
|
+
sdkVersion: this.sdkVersion,
|
|
7694
|
+
groupId,
|
|
7695
|
+
channel,
|
|
7696
|
+
},
|
|
7697
|
+
};
|
|
7698
|
+
try {
|
|
7699
|
+
await this.sendEvent(event);
|
|
7700
|
+
this.logger.info(`[Analytics] Share click tracked for product: ${productId}`);
|
|
7701
|
+
}
|
|
7702
|
+
catch (error) {
|
|
7703
|
+
this.logger.error("[Analytics] Failed to track share click", error);
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
/**
|
|
7707
|
+
* Track group creation attempt event
|
|
7708
|
+
*/
|
|
7709
|
+
async trackGroupCreateAttempt(productId) {
|
|
7710
|
+
const event = {
|
|
7711
|
+
event: "GROUP_CREATE_ATTEMPT",
|
|
7712
|
+
productId,
|
|
7713
|
+
sessionId: this.sessionId,
|
|
7714
|
+
timestamp: new Date().toISOString(),
|
|
7715
|
+
context: {
|
|
7716
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7717
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7718
|
+
sdkVersion: this.sdkVersion,
|
|
7719
|
+
},
|
|
7720
|
+
};
|
|
7721
|
+
try {
|
|
7722
|
+
await this.sendEvent(event);
|
|
7723
|
+
this.logger.info(`[Analytics] Group create attempt tracked for product: ${productId}`);
|
|
7724
|
+
}
|
|
7725
|
+
catch (error) {
|
|
7726
|
+
this.logger.error("[Analytics] Failed to track group create attempt", error);
|
|
7727
|
+
}
|
|
7728
|
+
}
|
|
7729
|
+
/**
|
|
7730
|
+
* Track successful group creation event
|
|
7731
|
+
*/
|
|
7732
|
+
async trackGroupCreateSuccess(productId, groupId) {
|
|
7733
|
+
const event = {
|
|
7734
|
+
event: "GROUP_CREATE_SUCCESS",
|
|
7735
|
+
productId,
|
|
7736
|
+
sessionId: this.sessionId,
|
|
7737
|
+
timestamp: new Date().toISOString(),
|
|
7738
|
+
context: {
|
|
7739
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7740
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7741
|
+
sdkVersion: this.sdkVersion,
|
|
7742
|
+
groupId,
|
|
7743
|
+
},
|
|
7744
|
+
};
|
|
7745
|
+
try {
|
|
7746
|
+
await this.sendEvent(event);
|
|
7747
|
+
this.logger.info(`[Analytics] Group create success tracked for product: ${productId}`);
|
|
7748
|
+
}
|
|
7749
|
+
catch (error) {
|
|
7750
|
+
this.logger.error("[Analytics] Failed to track group create success", error);
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
/**
|
|
7754
|
+
* Track group creation failure event
|
|
7755
|
+
*/
|
|
7756
|
+
async trackGroupCreateFailure(productId, errorCode, message) {
|
|
7757
|
+
const event = {
|
|
7758
|
+
event: "GROUP_CREATE_FAILURE",
|
|
7759
|
+
productId,
|
|
7760
|
+
sessionId: this.sessionId,
|
|
7761
|
+
timestamp: new Date().toISOString(),
|
|
7762
|
+
context: {
|
|
7763
|
+
pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
|
|
7764
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined,
|
|
7765
|
+
sdkVersion: this.sdkVersion,
|
|
7766
|
+
errorCode,
|
|
7767
|
+
errorMessage: message,
|
|
7768
|
+
},
|
|
7769
|
+
};
|
|
7770
|
+
try {
|
|
7771
|
+
await this.sendEvent(event);
|
|
7772
|
+
this.logger.info(`[Analytics] Group create failure tracked for product: ${productId}`);
|
|
7773
|
+
}
|
|
7774
|
+
catch (error) {
|
|
7775
|
+
this.logger.error("[Analytics] Failed to track group create failure", error);
|
|
7776
|
+
}
|
|
7777
|
+
}
|
|
7151
7778
|
/**
|
|
7152
7779
|
* Track custom event (extensible for future use)
|
|
7153
7780
|
*/
|
|
@@ -12593,8 +13220,14 @@ class CoBuy {
|
|
|
12593
13220
|
participantsCount !== undefined &&
|
|
12594
13221
|
participantsCount >= maxParticipants);
|
|
12595
13222
|
if (isFulfilled && productId && groupId) {
|
|
12596
|
-
//
|
|
12597
|
-
this.
|
|
13223
|
+
// Only prepare checkout if the current user is actually a member of this group.
|
|
13224
|
+
// this.widgets tracks all WidgetRoot instances; getJoinedGroupId() returns the
|
|
13225
|
+
// joined group ID only when the user has completed a join this session (not for
|
|
13226
|
+
// observers). Calling prepareCheckout for non-members would generate spurious 403s.
|
|
13227
|
+
const userIsInGroup = [...this.widgets].some((w) => w.getJoinedGroupId() === groupId);
|
|
13228
|
+
if (userIsInGroup) {
|
|
13229
|
+
this.prepareCheckoutIfNotDone(productId, groupId);
|
|
13230
|
+
}
|
|
12598
13231
|
}
|
|
12599
13232
|
// Emit user-defined callback if provided
|
|
12600
13233
|
if ((_a = config.events) === null || _a === void 0 ? void 0 : _a.onGroupMemberJoined) {
|
|
@@ -12607,8 +13240,11 @@ class CoBuy {
|
|
|
12607
13240
|
this.logger.warn("[SDK] Failed to initialize sockets", e);
|
|
12608
13241
|
}
|
|
12609
13242
|
}
|
|
12610
|
-
// Track page view
|
|
13243
|
+
// Track session init + page view events after successful initialization
|
|
12611
13244
|
if (this.analyticsClient) {
|
|
13245
|
+
this.analyticsClient.trackSessionInit(config.geo, config.device).catch((error) => {
|
|
13246
|
+
this.logger.warn("[SDK] Failed to track session init", error);
|
|
13247
|
+
});
|
|
12612
13248
|
this.analyticsClient.trackPageView().catch((error) => {
|
|
12613
13249
|
// Non-blocking: Analytics failure should not affect SDK initialization
|
|
12614
13250
|
this.logger.warn("[SDK] Failed to track page view", error);
|
|
@@ -12914,6 +13550,63 @@ class CoBuy {
|
|
|
12914
13550
|
this.logger.error("Error setting contact information", error);
|
|
12915
13551
|
}
|
|
12916
13552
|
}
|
|
13553
|
+
/**
|
|
13554
|
+
* Resolve an invite token to get product and group information
|
|
13555
|
+
*
|
|
13556
|
+
* Resolves an invite link token (e.g., from https://brand.cobuyza.co.za/i/QMeHD7ew)
|
|
13557
|
+
* to get the associated product, group, and invite metadata. Use this to handle
|
|
13558
|
+
* invite links in your application routing.
|
|
13559
|
+
*
|
|
13560
|
+
* @param inviteToken - The invite token from the URL
|
|
13561
|
+
* @returns Promise with invite resolution data or null if failed
|
|
13562
|
+
*
|
|
13563
|
+
* @example
|
|
13564
|
+
* ```typescript
|
|
13565
|
+
* // Extract token from URL path like /i/QMeHD7ew
|
|
13566
|
+
* const pathParts = window.location.pathname.split('/i/');
|
|
13567
|
+
* if (pathParts.length > 1) {
|
|
13568
|
+
* const inviteToken = pathParts[1];
|
|
13569
|
+
* const inviteData = await CoBuy.resolveInvite(inviteToken);
|
|
13570
|
+
*
|
|
13571
|
+
* if (inviteData) {
|
|
13572
|
+
* // Navigate to product page
|
|
13573
|
+
* window.location.href = `/product/${inviteData.product.id}`;
|
|
13574
|
+
* }
|
|
13575
|
+
* }
|
|
13576
|
+
* ```
|
|
13577
|
+
*/
|
|
13578
|
+
async resolveInvite(inviteToken) {
|
|
13579
|
+
if (!this.configManager.isInitialized()) {
|
|
13580
|
+
this.logger.warn("SDK not initialized, cannot resolve invite");
|
|
13581
|
+
return null;
|
|
13582
|
+
}
|
|
13583
|
+
if (!this.apiClient) {
|
|
13584
|
+
this.logger.error("API client not available");
|
|
13585
|
+
return null;
|
|
13586
|
+
}
|
|
13587
|
+
if (!inviteToken || typeof inviteToken !== "string" || inviteToken.trim() === "") {
|
|
13588
|
+
this.logger.error("Invalid invite token provided");
|
|
13589
|
+
return null;
|
|
13590
|
+
}
|
|
13591
|
+
try {
|
|
13592
|
+
const response = await this.apiClient.resolveInvite(inviteToken.trim());
|
|
13593
|
+
if (response.success && response.data) {
|
|
13594
|
+
this.logger.info(`Invite resolved successfully: product=${response.data.product.id}, group=${response.data.group.id}`);
|
|
13595
|
+
return response.data;
|
|
13596
|
+
}
|
|
13597
|
+
else {
|
|
13598
|
+
if (this.handleFraudError(response.error, "resolveInvite")) {
|
|
13599
|
+
return null;
|
|
13600
|
+
}
|
|
13601
|
+
this.logger.error("Failed to resolve invite", response.error);
|
|
13602
|
+
return null;
|
|
13603
|
+
}
|
|
13604
|
+
}
|
|
13605
|
+
catch (error) {
|
|
13606
|
+
this.logger.error("Error resolving invite", error);
|
|
13607
|
+
return null;
|
|
13608
|
+
}
|
|
13609
|
+
}
|
|
12917
13610
|
/**
|
|
12918
13611
|
* Validate checkout for a group
|
|
12919
13612
|
*
|