@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.
@@ -1,4 +1,5 @@
1
1
  import { ApiClient } from "./api-client";
2
+ import { GeoData, DeviceData } from "./types";
2
3
  /**
3
4
  * Analytics event interface matching backend expectations
4
5
  */
@@ -11,6 +12,12 @@ export interface AnalyticsEvent {
11
12
  pageUrl?: string;
12
13
  userAgent?: string;
13
14
  sdkVersion?: string;
15
+ groupId?: string;
16
+ channel?: string;
17
+ totalMembers?: number;
18
+ errorCode?: string;
19
+ errorMessage?: string;
20
+ [key: string]: unknown;
14
21
  };
15
22
  }
16
23
  /**
@@ -35,10 +42,58 @@ export declare class AnalyticsClient {
35
42
  * @internal
36
43
  */
37
44
  private sendEvent;
45
+ /**
46
+ * Track session init event fired when the SDK initializes
47
+ */
48
+ trackSessionInit(geo?: GeoData, device?: DeviceData): Promise<void>;
38
49
  /**
39
50
  * Track page view event
40
51
  */
41
52
  trackPageView(): Promise<void>;
53
+ /**
54
+ * Track creative view event (widget rendered and visible)
55
+ */
56
+ trackCreativeView(productId: string): Promise<void>;
57
+ /**
58
+ * Track popup/lobby modal open event
59
+ */
60
+ trackPopupOpen(productId: string, groupId?: string): Promise<void>;
61
+ /**
62
+ * Track join attempt event (before API call)
63
+ */
64
+ trackJoinAttempt(productId: string, groupId: string): Promise<void>;
65
+ /**
66
+ * Track successful group join event
67
+ */
68
+ trackJoinSuccess(productId: string, groupId: string): Promise<void>;
69
+ /**
70
+ * Track join failure event
71
+ */
72
+ trackJoinFailure(productId: string, groupId?: string, errorCode?: string, message?: string): Promise<void>;
73
+ /**
74
+ * Track already joined event (user attempts to join a group they're already in)
75
+ */
76
+ trackAlreadyJoined(productId: string, groupId?: string): Promise<void>;
77
+ /**
78
+ * Track group full view event (user sees a fulfilled/full group)
79
+ */
80
+ trackGroupFullView(productId: string, groupId?: string, totalMembers?: number): Promise<void>;
81
+ /**
82
+ * Track share click event
83
+ */
84
+ trackShareClick(productId: string, groupId?: string, channel?: string): Promise<void>;
85
+ /**
86
+ * Track group creation attempt event
87
+ */
88
+ trackGroupCreateAttempt(productId: string): Promise<void>;
89
+ /**
90
+ * Track successful group creation event
91
+ */
92
+ trackGroupCreateSuccess(productId: string, groupId: string): Promise<void>;
93
+ /**
94
+ * Track group creation failure event
95
+ */
96
+ trackGroupCreateFailure(productId: string, errorCode?: string, message?: string): Promise<void>;
42
97
  /**
43
98
  * Track custom event (extensible for future use)
44
99
  */
@@ -1,4 +1,4 @@
1
- import { ApiRequestOptions, ApiResponse, ApiClientConfig, ProductRewardData, ProductPrimaryGroupData, GroupJoinResponseData, GroupInviteResponseData, InviteResolveResponseData, AuthStrategy, ShareChannel, Contact, CheckoutValidationData, CheckoutConfirmData } from "./types";
1
+ import { ApiRequestOptions, ApiResponse, ApiClientConfig, ProductRewardData, ProductContextData, ProductPrimaryGroupData, GroupJoinResponseData, GroupInviteResponseData, InviteResolveResponseData, AuthStrategy, ShareChannel, Contact, LeaveGroupResponseData, RecoverOrJoinGroupResponseData, CheckoutValidationData, CheckoutConfirmData } from "./types";
2
2
  /**
3
3
  * HTTP client for communicating with CoBuy API
4
4
  *
@@ -17,8 +17,11 @@ export declare class ApiClient {
17
17
  private botdScore;
18
18
  private traceId;
19
19
  private readonly rewardCache;
20
+ private readonly productContextCache;
20
21
  private readonly REWARD_CACHE_TTL;
21
- private readonly pendingRequests;
22
+ private readonly PRODUCT_CONTEXT_CACHE_TTL;
23
+ private readonly pendingRewardRequests;
24
+ private readonly pendingContextRequests;
22
25
  constructor(config: ApiClientConfig);
23
26
  /**
24
27
  * Update the BotD (Bot Detection) score for fraud detection
@@ -168,6 +171,18 @@ export declare class ApiClient {
168
171
  * }
169
172
  */
170
173
  getTraceId(): string | null;
174
+ private getCachedProductContext;
175
+ private normalizePrimaryGroup;
176
+ private buildRewardDataFromContext;
177
+ private setProductContextCache;
178
+ /**
179
+ * Get product context information
180
+ *
181
+ * Uses the consolidated bootstrap endpoint so the SDK can resolve campaign,
182
+ * reward, primary group, and active groups in a single request.
183
+ */
184
+ getProductContext(productId: string): Promise<ApiResponse<ProductContextData>>;
185
+ private fetchProductContext;
171
186
  /**
172
187
  * Get product reward information
173
188
  *
@@ -431,6 +446,22 @@ export declare class ApiClient {
431
446
  * ```
432
447
  */
433
448
  setContact(contact: Contact): Promise<ApiResponse<void>>;
449
+ /**
450
+ * Leave an active group for the current SDK session.
451
+ */
452
+ leaveGroup(groupId: string, params?: {
453
+ leave_source?: "close_icon" | "browser_close" | "unknown";
454
+ leave_reason?: "voluntary_with_contact" | "voluntary_no_contact" | "unknown";
455
+ }): Promise<ApiResponse<LeaveGroupResponseData>>;
456
+ recoverOrJoinGroup(productId: string, contact?: Contact): Promise<ApiResponse<RecoverOrJoinGroupResponseData>>;
457
+ /**
458
+ * Best-effort leave signal for unload/pagehide scenarios.
459
+ * Uses fetch keepalive so it can run while page is closing.
460
+ */
461
+ leaveGroupBestEffort(groupId: string, params?: {
462
+ leave_source?: "close_icon" | "browser_close" | "unknown";
463
+ leave_reason?: "voluntary_with_contact" | "voluntary_no_contact" | "unknown";
464
+ }): void;
434
465
  /**
435
466
  * Prepare checkout for a group
436
467
  *
@@ -2,9 +2,12 @@
2
2
  * API endpoint paths for active CoBuy backend endpoints
3
3
  */
4
4
  export declare const API_ENDPOINTS: {
5
+ readonly PRODUCT_CONTEXT: "/v1/sdk/products/:productId/context";
5
6
  readonly PRODUCT_REWARD: "/v1/sdk/products/:productId/reward";
6
7
  readonly PRODUCT_PRIMARY_GROUP: "/v1/sdk/products/:productId/group/primary";
8
+ readonly PRODUCT_RECOVER_OR_JOIN_GROUP: "/v1/sdk/products/:productId/group/recover-or-join";
7
9
  readonly GROUP_JOIN: "/v1/sdk/groups/:groupId/join";
10
+ readonly GROUP_LEAVE: "/v1/sdk/groups/:groupId/leave";
8
11
  readonly GROUP_CREATE_AND_JOIN: "/v1/sdk/groups/new/join";
9
12
  readonly PRODUCT_ACTIVE_GROUPS: "/v1/sdk/products/:productId/groups/active";
10
13
  readonly GROUP_INVITE: "/v1/sdk/groups/:groupId/invite";
@@ -1,7 +1,8 @@
1
- import { InternalConfig, GroupFulfilledEvent, GroupCreatedEvent, GroupMemberJoinedEvent } from "./types";
2
- export type SocketEventName = "group:member:joined" | "group:created" | "group:fulfilled";
1
+ import { InternalConfig, GroupFulfilledEvent, GroupCreatedEvent, GroupMemberJoinedEvent, GroupMemberLeftEvent } from "./types";
2
+ export type SocketEventName = "group:member:joined" | "group:member:left" | "group:created" | "group:fulfilled";
3
3
  export interface SocketHandlers {
4
4
  onGroupMemberJoined?: (payload: GroupMemberJoinedEvent) => void;
5
+ onGroupMemberLeft?: (payload: GroupMemberLeftEvent) => void;
5
6
  onGroupCreated?: (payload: GroupCreatedEvent) => void;
6
7
  onGroupFulfilled?: (payload: GroupFulfilledEvent) => void;
7
8
  }
@@ -126,6 +126,29 @@ export interface PerformanceConfig {
126
126
  /**
127
127
  * Options for initializing the CoBuy SDK
128
128
  */
129
+ /**
130
+ * Geographic data provided by the merchant at SDK init time
131
+ */
132
+ export interface GeoData {
133
+ latitude?: number;
134
+ longitude?: number;
135
+ city?: string;
136
+ region?: string;
137
+ country?: string;
138
+ [key: string]: unknown;
139
+ }
140
+ /**
141
+ * Device data provided by the merchant at SDK init time
142
+ */
143
+ export interface DeviceData {
144
+ /** "mobile" | "tablet" | "desktop" */
145
+ type?: string;
146
+ os?: string;
147
+ browser?: string;
148
+ screenWidth?: number;
149
+ screenHeight?: number;
150
+ [key: string]: unknown;
151
+ }
129
152
  export interface CoBuyInitOptions {
130
153
  /**
131
154
  * Authentication mode (default: "public")
@@ -207,6 +230,16 @@ export interface CoBuyInitOptions {
207
230
  * ```
208
231
  */
209
232
  performance?: PerformanceConfig;
233
+ /**
234
+ * Optional geographic data for analytics (merchant-provided)
235
+ * Use when the merchant's backend has resolved geo info from IP or user input
236
+ */
237
+ geo?: GeoData;
238
+ /**
239
+ * Optional device data for analytics (merchant-provided)
240
+ * Collect from browser APIs (navigator, screen) and pass here
241
+ */
242
+ device?: DeviceData;
210
243
  }
211
244
  /**
212
245
  * Callback data structure for checkout events
@@ -272,6 +305,7 @@ export interface FulfilledGroup {
272
305
  campaign_id: string;
273
306
  campaign_name: string;
274
307
  campaign_status: string;
308
+ campaign_redemption_method?: "online" | "offline" | "both";
275
309
  participants_count: number;
276
310
  max_participants: number;
277
311
  status: string;
@@ -453,6 +487,10 @@ export interface ModalOptions {
453
487
  * Offline redemption data for fulfilled groups
454
488
  */
455
489
  offlineRedemption?: OfflineRedemption;
490
+ /**
491
+ * Campaign redemption method - controls which checkout options are shown
492
+ */
493
+ redemptionMethod?: "online" | "offline" | "both";
456
494
  /**
457
495
  * Optional callback when modal opens
458
496
  */
@@ -503,7 +541,7 @@ export interface Reward {
503
541
  id: string;
504
542
  name: string;
505
543
  type: RewardType;
506
- value: string;
544
+ value: string | number;
507
545
  description: string | null;
508
546
  remaining_quantity: number;
509
547
  total_quantity: number;
@@ -523,14 +561,56 @@ export interface RewardEligibility {
523
561
  */
524
562
  export interface ProductRewardData {
525
563
  productId: string;
526
- reward: Reward;
564
+ reward: Reward | null;
565
+ campaign_mode?: "group" | "no_group";
566
+ campaign_creative_id?: string | null;
567
+ eligibility: RewardEligibility;
568
+ }
569
+ export interface ProductContextProductData {
570
+ id: string;
571
+ external_product_id: string | null;
572
+ name: string;
573
+ description: string | null;
574
+ sku: string | null;
575
+ price: number;
576
+ images: unknown[];
577
+ }
578
+ export interface ProductContextCampaignData {
579
+ id: string;
580
+ name: string;
581
+ description: string | null;
582
+ status: string | null;
583
+ campaign_mode?: "group" | "no_group" | null;
584
+ min_group_size?: number | null;
585
+ total_groups_count?: number | null;
586
+ reward_type?: string | null;
587
+ reward_value?: string | number | null;
588
+ start_date?: string | null;
589
+ end_date?: string | null;
590
+ redemption_method?: "online" | "offline" | "both" | null;
591
+ }
592
+ export interface ProductContextData {
593
+ product_id: string;
594
+ product: ProductContextProductData | null;
595
+ campaign: ProductContextCampaignData | null;
596
+ reward: Reward | null;
597
+ primary_group: ProductPrimaryGroupData | null;
598
+ active_groups: Array<ProductPrimaryGroupData & {
599
+ is_member?: boolean;
600
+ }>;
527
601
  eligibility: RewardEligibility;
602
+ sdk: {
603
+ ready: boolean;
604
+ campaign_status: string | null;
605
+ has_primary_group: boolean;
606
+ active_group_count: number;
607
+ };
528
608
  }
529
609
  /**
530
610
  * Primary group information for a product (SCRUM-284)
531
611
  */
532
612
  export interface ProductPrimaryGroupData {
533
- group: ProductPrimaryGroupData | PromiseLike<ProductPrimaryGroupData | null> | null;
613
+ group?: ProductPrimaryGroupData | null;
534
614
  id: string;
535
615
  group_name: string;
536
616
  description: string | null;
@@ -539,6 +619,7 @@ export interface ProductPrimaryGroupData {
539
619
  campaign_id: string | null;
540
620
  campaign_name: string | null;
541
621
  campaign_status: string | null;
622
+ campaign_redemption_method?: "online" | "offline" | "both";
542
623
  participants_count: number;
543
624
  max_participants: number;
544
625
  status: string;
@@ -558,6 +639,7 @@ export interface GroupJoinResponseData {
558
639
  status: string;
559
640
  expiry_at: string;
560
641
  timeLeftSeconds: number;
642
+ campaign_redemption_method?: "online" | "offline" | "both";
561
643
  };
562
644
  membership: {
563
645
  id: string;
@@ -568,6 +650,37 @@ export interface GroupJoinResponseData {
568
650
  product_id: string;
569
651
  offline_redemption?: OfflineRedemption;
570
652
  }
653
+ export interface LeaveGroupResponseData {
654
+ left: boolean;
655
+ session_id: string;
656
+ group: {
657
+ id: string;
658
+ group_name: string;
659
+ description: string | null;
660
+ campaign_creative_id: string | null;
661
+ campaign_creative_name: string | null;
662
+ campaign_id: string | null;
663
+ campaign_name: string | null;
664
+ campaign_status: string | null;
665
+ campaign_redemption_method?: "online" | "offline" | "both";
666
+ participants_count: number;
667
+ max_participants: number;
668
+ status: string;
669
+ expiry_at: string;
670
+ timeLeftSeconds: number;
671
+ };
672
+ product_id?: string;
673
+ }
674
+ export interface RecoverOrJoinGroupResponseData extends GroupJoinResponseData {
675
+ recovery: {
676
+ recovered: boolean;
677
+ matched_by: "session" | "contact" | "none";
678
+ outcome: "existing_active_membership" | "rejoined_previous_group" | "joined_next_available_group" | "created_new_group";
679
+ previous_group_id?: string;
680
+ joined_group_id: string;
681
+ };
682
+ }
683
+ export type GroupMemberLeftEvent = LeaveGroupResponseData;
571
684
  export type ShareChannel = "whatsapp" | "facebook" | "email" | "sms" | "copy_link" | "tiktok" | "x" | "other";
572
685
  export interface GroupInviteResponseData {
573
686
  invite_url?: string;
@@ -768,6 +881,8 @@ export interface InternalConfig {
768
881
  maxRetries: number;
769
882
  animationSpeed: "fast" | "normal" | "slow";
770
883
  };
884
+ geo?: GeoData;
885
+ device?: DeviceData;
771
886
  }
772
887
  /**
773
888
  * Group status enumeration
@@ -1,4 +1,5 @@
1
1
  import { ApiClient } from "../../core/api-client";
2
+ import { AnalyticsClient } from "../../core/analytics";
2
3
  import { ProductPrimaryGroupData, GroupJoinResponseData } from "../../core/types";
3
4
  export interface GroupListItem {
4
5
  groupId: string;
@@ -27,12 +28,15 @@ export declare class GroupListModal {
27
28
  private currentJoinedGroupId;
28
29
  private currentProductId;
29
30
  private currentSessionId;
31
+ private analyticsClient;
30
32
  private onGroupJoined;
31
33
  private onViewProgress;
32
34
  private socketListenerRegistered;
33
35
  private readonly escapeHandler;
34
36
  private readonly handleGroupMemberJoinedEvent;
35
37
  constructor(groups?: GroupListItem[], liveCount?: number, debug?: boolean, apiClient?: ApiClient | null);
38
+ /** Set the analytics client for event tracking */
39
+ setAnalyticsClient(client: AnalyticsClient): void;
36
40
  /** Set callback for when a group is joined successfully */
37
41
  setOnGroupJoined(callback: (joinData: GroupJoinResponseData) => void): void;
38
42
  /** Set callback for when viewing progress on an already joined group */
@@ -1,4 +1,5 @@
1
1
  import { ApiClient } from "../../core/api-client";
2
+ import { AnalyticsClient } from "../../core/analytics";
2
3
  import { OfflineRedemption } from "../../core/types";
3
4
  import { SocketManager } from "../../core/socket";
4
5
  import "./styles/styles.css";
@@ -17,6 +18,7 @@ export interface LobbyModalData {
17
18
  activities?: ActivityItem[];
18
19
  isLocked?: boolean;
19
20
  offlineRedemption?: OfflineRedemption;
21
+ redemptionMethod?: "online" | "offline" | "both";
20
22
  }
21
23
  export interface ActivityItem {
22
24
  emoji: string;
@@ -31,6 +33,9 @@ export interface LobbyModalCallbacks {
31
33
  onCopyLink?: (link: string) => void;
32
34
  onShare?: () => void;
33
35
  }
36
+ interface CloseOptions {
37
+ skipLeaveFlow?: boolean;
38
+ }
34
39
  /**
35
40
  * LobbyModal - Renders and manages the group buying lobby modal
36
41
  */
@@ -38,6 +43,7 @@ export declare class LobbyModal {
38
43
  private readonly logger;
39
44
  private readonly socketManager;
40
45
  private readonly apiClient;
46
+ private readonly analyticsClient;
41
47
  private modalElement;
42
48
  private data;
43
49
  private readonly callbacks;
@@ -48,7 +54,36 @@ export declare class LobbyModal {
48
54
  private currentGroupId;
49
55
  private shareOverlay;
50
56
  private keyboardHandler;
51
- constructor(data: LobbyModalData, callbacks: LobbyModalCallbacks, apiClient: ApiClient | null, socketManager?: SocketManager | null, debug?: boolean);
57
+ private leaveFlowInProgress;
58
+ private leaveSignalSent;
59
+ private hasContactProtection;
60
+ private initialContactPromptTimer;
61
+ private contactPromptOverlay;
62
+ private leaveLoaderOverlay;
63
+ private beforeUnloadHandler;
64
+ private pageHideHandler;
65
+ private visibilityChangeHandler;
66
+ private readonly CONTACT_PROTECTION_PREFIX;
67
+ private readonly LAST_LEFT_GROUP_PREFIX;
68
+ constructor(data: LobbyModalData, callbacks: LobbyModalCallbacks, apiClient: ApiClient | null, socketManager?: SocketManager | null, analyticsClient?: AnalyticsClient | null, debug?: boolean);
69
+ private getSessionId;
70
+ private getContactProtectionStorageKey;
71
+ private getLastLeftGroupStorageKey;
72
+ private readContactProtectionState;
73
+ private persistContactProtectionState;
74
+ private persistLastLeftGroup;
75
+ private clearInitialContactPromptTimer;
76
+ private clearContactPromptOverlay;
77
+ private showBlockingLoader;
78
+ private hideBlockingLoader;
79
+ private updateContactProtectionUI;
80
+ private showToastMessage;
81
+ private trackAnalyticsEvent;
82
+ private saveContactValue;
83
+ private openContactProtectionPrompt;
84
+ private openUnprotectedLeaveWarning;
85
+ private scheduleInitialContactPrompt;
86
+ private leaveGroupExplicitly;
52
87
  /**
53
88
  * Derive lock state from data so UI reflects completion
54
89
  */
@@ -103,6 +138,10 @@ export declare class LobbyModal {
103
138
  * Create link section (just the link box and share button, no wrapper)
104
139
  */
105
140
  private createLinkSection;
141
+ /**
142
+ * Create a standalone "Checkout Online" button for the link/share section
143
+ */
144
+ private createOnlineCheckoutButton;
106
145
  /**
107
146
  * Create offline redemption section
108
147
  */
@@ -251,10 +290,16 @@ export declare class LobbyModal {
251
290
  * Clean up keyboard event listeners
252
291
  */
253
292
  private removeKeyboardAccessibility;
293
+ private registerLifecycleLeaveHandlers;
294
+ private unregisterLifecycleLeaveHandlers;
295
+ private triggerBestEffortLeave;
296
+ private shouldRunLeaveFlow;
297
+ private inferContactType;
298
+ private runLeaveFlowAndClose;
254
299
  /**
255
300
  * Close the modal
256
301
  */
257
- close(): void;
302
+ close(options?: CloseOptions): void;
258
303
  /**
259
304
  * Check if modal is open
260
305
  */
@@ -291,6 +336,7 @@ export declare class LobbyModal {
291
336
  * Subscribe to socket events
292
337
  */
293
338
  private subscribeToSocketEvents;
339
+ private unsubscribeFromSocketEvents;
294
340
  /**
295
341
  * Handle socket group update events
296
342
  */
@@ -308,3 +354,4 @@ export declare class LobbyModal {
308
354
  */
309
355
  private updateTeamCard;
310
356
  }
357
+ export {};
@@ -30,6 +30,7 @@ export declare class WidgetRoot {
30
30
  private groupExpiryRefreshTriggered;
31
31
  private offlineRedemption;
32
32
  private offlineRedemptionModal;
33
+ private campaignRedemptionMethod;
33
34
  private isRendering;
34
35
  private renderPromise;
35
36
  private liveRegionAnnouncer;
@@ -38,11 +39,16 @@ export declare class WidgetRoot {
38
39
  private renderDebounceTimer;
39
40
  private pendingRenderOptions;
40
41
  private readonly RENDER_DEBOUNCE_MS;
42
+ private readonly LAST_LEFT_GROUP_PREFIX;
41
43
  /**
42
44
  * Get max retries from config (configurable via performance options)
43
45
  */
44
46
  private get MAX_RETRIES();
45
47
  constructor(config: InternalConfig, apiClient: ApiClient | null, analyticsClient?: AnalyticsClient | null);
48
+ private getRecoveryStorageKey;
49
+ private getStoredRecoveryGroupId;
50
+ private clearStoredRecoveryGroupId;
51
+ private showRecoveryToast;
46
52
  /** Subscribe once to backend socket events routed through the host page */
47
53
  private subscribeToSocketEvents;
48
54
  /** Handle backend fulfillment notifications */
@@ -185,4 +191,10 @@ export declare class WidgetRoot {
185
191
  * Expose current product id for optional filtering by host
186
192
  */
187
193
  getProductId(): string | null;
194
+ /**
195
+ * Returns the group ID that the current user has joined via this widget, or null if the user
196
+ * is only an observer (has not actively joined a group this session).
197
+ * Used by the CoBuy host class to check membership before preparing checkout.
198
+ */
199
+ getJoinedGroupId(): string | null;
188
200
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cshah18/sdk",
3
- "version": "4.13.0",
3
+ "version": "4.15.0",
4
4
  "description": "CoBuy Embedded SDK for browser JavaScript integration",
5
5
  "type": "module",
6
6
  "main": "dist/cobuy-sdk.umd.js",