@cshah18/sdk 4.13.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.
@@ -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
- // If socket is available and connected, subscribe to this group
3582
- if (this.socketManager && this.socketManager.isConnected() && this.currentGroupId) {
3583
- this.logger.info(`[LobbyModal] Socket connected, subscribing to group ${this.currentGroupId}`);
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.productId) {
4438
+ if (!detail || !detail.product_id) {
4439
+ return;
4440
+ }
4441
+ if (this.currentProductId && detail.product_id !== this.currentProductId) {
4390
4442
  return;
4391
4443
  }
4392
- if (this.currentProductId && detail.productId !== this.currentProductId) {
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 ((_d = (_c = this.currentRewardData) === null || _c === void 0 ? void 0 : _c.reward) === null || _d === void 0 ? void 0 : _d.value) {
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
@@ -6038,8 +6159,11 @@ class ApiClient {
6038
6159
  var _a;
6039
6160
  this.traceId = null;
6040
6161
  this.rewardCache = new Map();
6162
+ this.productContextCache = new Map();
6041
6163
  this.REWARD_CACHE_TTL = 60000; // 1 minute
6042
- this.pendingRequests = new Map();
6164
+ this.PRODUCT_CONTEXT_CACHE_TTL = 30000; // 30 seconds
6165
+ this.pendingRewardRequests = new Map();
6166
+ this.pendingContextRequests = new Map();
6043
6167
  this.baseUrl = config.baseUrl;
6044
6168
  this.authStrategy = config.authStrategy;
6045
6169
  this.sessionId = config.sessionId;
@@ -6435,6 +6559,106 @@ class ApiClient {
6435
6559
  getTraceId() {
6436
6560
  return this.traceId;
6437
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
+ }
6438
6662
  /**
6439
6663
  * Get product reward information
6440
6664
  *
@@ -6479,13 +6703,31 @@ class ApiClient {
6479
6703
  data: cached.data,
6480
6704
  };
6481
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
+ }
6482
6714
  const cacheKey = `reward:${productId}`;
6483
- const pending = this.pendingRequests.get(cacheKey);
6715
+ const pending = this.pendingRewardRequests.get(cacheKey);
6484
6716
  if (pending) {
6485
6717
  this.logger.info(`Deduplicating request for product: ${productId}`);
6486
6718
  return pending;
6487
6719
  }
6488
- const promise = this.fetchProductReward(productId).then((response) => {
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) => {
6489
6731
  if (response.success && response.data) {
6490
6732
  this.rewardCache.set(productId, {
6491
6733
  data: response.data,
@@ -6494,8 +6736,8 @@ class ApiClient {
6494
6736
  }
6495
6737
  return response;
6496
6738
  });
6497
- this.pendingRequests.set(cacheKey, promise);
6498
- promise.then(() => this.pendingRequests.delete(cacheKey), () => this.pendingRequests.delete(cacheKey));
6739
+ this.pendingRewardRequests.set(cacheKey, promise);
6740
+ promise.then(() => this.pendingRewardRequests.delete(cacheKey), () => this.pendingRewardRequests.delete(cacheKey));
6499
6741
  return promise;
6500
6742
  }
6501
6743
  /**
@@ -6511,6 +6753,7 @@ class ApiClient {
6511
6753
  */
6512
6754
  clearRewardCache() {
6513
6755
  this.rewardCache.clear();
6756
+ this.productContextCache.clear();
6514
6757
  }
6515
6758
  async fetchProductReward(productId) {
6516
6759
  var _a;
@@ -6558,7 +6801,7 @@ class ApiClient {
6558
6801
  * ```
6559
6802
  */
6560
6803
  async getProductPrimaryGroup(productId) {
6561
- var _a;
6804
+ var _a, _b;
6562
6805
  if (!validateProductId(productId)) {
6563
6806
  return {
6564
6807
  success: false,
@@ -6568,15 +6811,38 @@ class ApiClient {
6568
6811
  },
6569
6812
  };
6570
6813
  }
6571
- const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_PRIMARY_GROUP, { productId }, { allowAutoCreate: true });
6572
- this.logger.info(`Fetching primary group for product: ${productId}`);
6573
- const response = await this.get(endpoint);
6574
- if (response.success && ((_a = response.data) === null || _a === void 0 ? void 0 : _a.data)) {
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) {
6575
6829
  return {
6576
6830
  success: true,
6577
- data: response.data.data,
6831
+ data: Object.assign(Object.assign({}, contextGroup), { group: contextGroup }),
6578
6832
  };
6579
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
+ }
6580
6846
  return {
6581
6847
  success: false,
6582
6848
  error: response.error || {
@@ -6624,6 +6890,25 @@ class ApiClient {
6624
6890
  },
6625
6891
  };
6626
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
+ }
6627
6912
  const endpoint = buildApiUrl("", API_ENDPOINTS.PRODUCT_ACTIVE_GROUPS, { productId });
6628
6913
  this.logger.info(`Fetching active groups for product: ${productId}`);
6629
6914
  const response = await this.get(endpoint);
@@ -7128,6 +7413,7 @@ class AnalyticsClient {
7128
7413
  const event = {
7129
7414
  event: "CTA_CLICKED",
7130
7415
  productId,
7416
+ sessionId: this.sessionId,
7131
7417
  timestamp: new Date().toISOString(),
7132
7418
  context: {
7133
7419
  pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
@@ -7180,12 +7466,32 @@ class AnalyticsClient {
7180
7466
  throw new CoBuyApiError(error instanceof Error ? error.message : "Unknown analytics error", "ANALYTICS_ERROR", { originalError: error });
7181
7467
  }
7182
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
+ }
7183
7488
  /**
7184
7489
  * Track page view event
7185
7490
  */
7186
7491
  async trackPageView() {
7187
7492
  const event = {
7188
7493
  event: "PAGE_VIEW",
7494
+ sessionId: this.sessionId,
7189
7495
  timestamp: new Date().toISOString(),
7190
7496
  context: {
7191
7497
  pageUrl: typeof window !== "undefined" ? window.location.href : undefined,
@@ -7202,6 +7508,273 @@ class AnalyticsClient {
7202
7508
  this.logger.error("[Analytics] Failed to track page view", error);
7203
7509
  }
7204
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
+ }
7205
7778
  /**
7206
7779
  * Track custom event (extensible for future use)
7207
7780
  */
@@ -12647,8 +13220,14 @@ class CoBuy {
12647
13220
  participantsCount !== undefined &&
12648
13221
  participantsCount >= maxParticipants);
12649
13222
  if (isFulfilled && productId && groupId) {
12650
- // Prepare checkout when group is fulfilled (if not already prepared)
12651
- this.prepareCheckoutIfNotDone(productId, groupId);
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
+ }
12652
13231
  }
12653
13232
  // Emit user-defined callback if provided
12654
13233
  if ((_a = config.events) === null || _a === void 0 ? void 0 : _a.onGroupMemberJoined) {
@@ -12661,8 +13240,11 @@ class CoBuy {
12661
13240
  this.logger.warn("[SDK] Failed to initialize sockets", e);
12662
13241
  }
12663
13242
  }
12664
- // Track page view event after successful initialization
13243
+ // Track session init + page view events after successful initialization
12665
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
+ });
12666
13248
  this.analyticsClient.trackPageView().catch((error) => {
12667
13249
  // Non-blocking: Analytics failure should not affect SDK initialization
12668
13250
  this.logger.warn("[SDK] Failed to track page view", error);