@product7/product7-js 0.3.2 → 0.3.4

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.
@@ -737,36 +737,7 @@
737
737
  }
738
738
 
739
739
  async identifyContact(data) {
740
- await this.api._ensureSession();
741
-
742
- if (this.api.mock) {
743
- await delay$1(300);
744
- return {
745
- status: true,
746
- data: {
747
- contact_id: 'mock_contact_' + Date.now(),
748
- email: data.email,
749
- name: data.name || '',
750
- is_new: true,
751
- },
752
- };
753
- }
754
-
755
- return this.api._makeRequest('/widget/messenger/identify', {
756
- method: 'POST',
757
- headers: {
758
- 'Content-Type': 'application/json',
759
- Authorization: `Bearer ${this.api.sessionToken}`,
760
- },
761
- body: JSON.stringify({
762
- email: data.email,
763
- name: data.name || '',
764
- phone: data.phone || '',
765
- company: data.company || '',
766
- avatar_url: data.avatar_url || '',
767
- metadata: data.metadata || {},
768
- }),
769
- });
740
+ return this.api.identify(data);
770
741
  }
771
742
 
772
743
  async sendTypingIndicator(conversationId, isTyping) {
@@ -995,11 +966,16 @@
995
966
  this.sessionToken = null;
996
967
  this.sessionExpiry = null;
997
968
  this.metadata = config.metadata || null;
969
+ this.identitySyncedToken = null;
998
970
  this.mock = config.mock || false;
999
971
  this.env = config.env || 'production';
1000
972
  this.baseURL = this._getBaseURL(config);
1001
973
 
1002
974
  this._loadStoredSession();
975
+ this._loadStoredMetadata();
976
+ if (this.isSessionValid() && this.metadata) {
977
+ this.identitySyncedToken = this.sessionToken;
978
+ }
1003
979
  }
1004
980
 
1005
981
  _getBaseURL(config) {
@@ -1039,20 +1015,17 @@
1039
1015
  : envConfig.base;
1040
1016
  }
1041
1017
 
1042
- async init(metadata = null) {
1043
- if (metadata) {
1044
- this.metadata = metadata;
1018
+ async init(metadata = undefined) {
1019
+ if (metadata !== undefined) {
1020
+ this.setMetadata(metadata);
1045
1021
  }
1046
1022
 
1047
1023
  if (this.isSessionValid()) {
1048
1024
  return { sessionToken: this.sessionToken };
1049
1025
  }
1050
1026
 
1051
- if (!this.workspace || !this.metadata) {
1052
- throw new APIError$1(
1053
- 400,
1054
- `Missing ${!this.workspace ? 'workspace' : 'user context'} for initialization`
1055
- );
1027
+ if (!this.workspace) {
1028
+ throw new APIError$1(400, 'Missing workspace for initialization');
1056
1029
  }
1057
1030
 
1058
1031
  if (this.mock) {
@@ -1065,6 +1038,7 @@
1065
1038
  async _initMockSession() {
1066
1039
  this.sessionToken = 'mock_session_' + Date.now();
1067
1040
  this.sessionExpiry = new Date(Date.now() + 3600 * 1000);
1041
+ this.identitySyncedToken = null;
1068
1042
  this._storeSession();
1069
1043
  return {
1070
1044
  sessionToken: this.sessionToken,
@@ -1083,7 +1057,6 @@
1083
1057
  async _initRealSession() {
1084
1058
  const payload = {
1085
1059
  workspace: this.workspace,
1086
- user: this.metadata,
1087
1060
  };
1088
1061
 
1089
1062
  try {
@@ -1096,6 +1069,7 @@
1096
1069
 
1097
1070
  this.sessionToken = initData.sessionToken;
1098
1071
  this.sessionExpiry = new Date(Date.now() + initData.expiresIn * 1000);
1072
+ this.identitySyncedToken = null;
1099
1073
  this._storeSession();
1100
1074
 
1101
1075
  return {
@@ -1162,12 +1136,52 @@
1162
1136
  this.sessionToken = null;
1163
1137
  this.sessionExpiry = null;
1164
1138
  await this.init();
1139
+ await this._restoreIdentity();
1165
1140
  return await method.apply(this, args);
1166
1141
  }
1167
1142
  throw error;
1168
1143
  }
1169
1144
  }
1170
1145
 
1146
+ async identify(metadata = this.metadata) {
1147
+ if (metadata !== undefined) {
1148
+ this.setMetadata(metadata);
1149
+ }
1150
+
1151
+ if (!this.metadata) {
1152
+ throw new APIError$1(400, 'Missing user context for identify');
1153
+ }
1154
+
1155
+ await this._ensureSession();
1156
+
1157
+ const payload = this._buildIdentifyPayload(this.metadata);
1158
+
1159
+ if (this.mock) {
1160
+ this.identitySyncedToken = this.sessionToken;
1161
+ return {
1162
+ status: true,
1163
+ identified: true,
1164
+ data: {
1165
+ user_id: payload.user_id || null,
1166
+ email: payload.email || null,
1167
+ name: payload.name || null,
1168
+ },
1169
+ };
1170
+ }
1171
+
1172
+ const response = await this._makeRequest('/widget/identify', {
1173
+ method: 'POST',
1174
+ body: JSON.stringify(payload),
1175
+ headers: {
1176
+ 'Content-Type': 'application/json',
1177
+ Authorization: `Bearer ${this.sessionToken}`,
1178
+ },
1179
+ });
1180
+
1181
+ this.identitySyncedToken = this.sessionToken;
1182
+ return response;
1183
+ }
1184
+
1171
1185
  isSessionValid() {
1172
1186
  return (
1173
1187
  this.sessionToken && this.sessionExpiry && new Date() < this.sessionExpiry
@@ -1175,8 +1189,13 @@
1175
1189
  }
1176
1190
 
1177
1191
  setMetadata(metadata) {
1178
- this.metadata = metadata;
1179
- this._storeData('product7_metadata', metadata);
1192
+ this.metadata = metadata || null;
1193
+ this.identitySyncedToken = null;
1194
+ if (this.metadata) {
1195
+ this._storeData('product7_metadata', this.metadata);
1196
+ } else {
1197
+ this._removeData('product7_metadata');
1198
+ }
1180
1199
  }
1181
1200
 
1182
1201
  getMetadata() {
@@ -1186,6 +1205,7 @@
1186
1205
  clearSession() {
1187
1206
  this.sessionToken = null;
1188
1207
  this.sessionExpiry = null;
1208
+ this.identitySyncedToken = null;
1189
1209
  this._removeData('product7_session');
1190
1210
  this._removeData('product7_metadata');
1191
1211
  }
@@ -1211,6 +1231,14 @@
1211
1231
  if (!stored) return false;
1212
1232
 
1213
1233
  const sessionData = JSON.parse(stored);
1234
+ if (
1235
+ this.workspace &&
1236
+ sessionData.workspace &&
1237
+ sessionData.workspace !== this.workspace
1238
+ ) {
1239
+ localStorage.removeItem('product7_session');
1240
+ return false;
1241
+ }
1214
1242
  this.sessionToken = sessionData.token;
1215
1243
  this.sessionExpiry = new Date(sessionData.expiry);
1216
1244
 
@@ -1220,6 +1248,32 @@
1220
1248
  }
1221
1249
  }
1222
1250
 
1251
+ _loadStoredMetadata() {
1252
+ if (this.metadata || typeof localStorage === 'undefined') return false;
1253
+
1254
+ try {
1255
+ const session = localStorage.getItem('product7_session');
1256
+ if (session) {
1257
+ const sessionData = JSON.parse(session);
1258
+ if (
1259
+ this.workspace &&
1260
+ sessionData.workspace &&
1261
+ sessionData.workspace !== this.workspace
1262
+ ) {
1263
+ return false;
1264
+ }
1265
+ }
1266
+
1267
+ const stored = localStorage.getItem('product7_metadata');
1268
+ if (!stored) return false;
1269
+
1270
+ this.metadata = JSON.parse(stored);
1271
+ return true;
1272
+ } catch (error) {
1273
+ return false;
1274
+ }
1275
+ }
1276
+
1223
1277
  _storeData(key, value) {
1224
1278
  if (typeof localStorage !== 'undefined') {
1225
1279
  localStorage.setItem(key, JSON.stringify(value));
@@ -1282,6 +1336,36 @@
1282
1336
  const queryString = this._buildQueryParams(params);
1283
1337
  return `${endpoint}${queryString ? '?' + queryString : ''}`;
1284
1338
  }
1339
+
1340
+ _buildIdentifyPayload(metadata = {}) {
1341
+ const payload = {
1342
+ user_id: metadata.user_id,
1343
+ email: metadata.email,
1344
+ name: metadata.name,
1345
+ avatar:
1346
+ metadata.profile_picture || metadata.avatar_url || metadata.avatar,
1347
+ attributes: metadata.custom_fields || {},
1348
+ };
1349
+
1350
+ if (metadata.company) {
1351
+ payload.company = metadata.company;
1352
+ }
1353
+
1354
+ return payload;
1355
+ }
1356
+
1357
+ async _restoreIdentity() {
1358
+ if (
1359
+ !this.metadata ||
1360
+ !this.sessionToken ||
1361
+ !this.identitySyncedToken ||
1362
+ this.identitySyncedToken === this.sessionToken
1363
+ ) {
1364
+ return;
1365
+ }
1366
+
1367
+ await this.identify(this.metadata);
1368
+ }
1285
1369
  }
1286
1370
 
1287
1371
  class APIService extends BaseAPIService {
@@ -1503,8 +1587,42 @@
1503
1587
  return this.messenger.submitRating(conversationId, data);
1504
1588
  }
1505
1589
 
1590
+ async identify(metadata) {
1591
+ await this._ensureSession();
1592
+
1593
+ if (this.mock) {
1594
+ await new Promise((r) => setTimeout(r, 300));
1595
+ return {
1596
+ status: true,
1597
+ data: {
1598
+ contact_id: 'mock_contact_' + Date.now(),
1599
+ email: metadata.email,
1600
+ name: metadata.name || '',
1601
+ is_new: true,
1602
+ },
1603
+ };
1604
+ }
1605
+
1606
+ return this._makeRequest('/widget/messenger/identify', {
1607
+ method: 'POST',
1608
+ headers: {
1609
+ 'Content-Type': 'application/json',
1610
+ Authorization: `Bearer ${this.sessionToken}`,
1611
+ },
1612
+ body: JSON.stringify({
1613
+ user_id: metadata.user_id || null,
1614
+ email: metadata.email || '',
1615
+ name: metadata.name || '',
1616
+ phone: metadata.phone || '',
1617
+ company: metadata.company || '',
1618
+ avatar_url: metadata.avatar_url || '',
1619
+ metadata: metadata.custom_fields || {},
1620
+ }),
1621
+ });
1622
+ }
1623
+
1506
1624
  async identifyContact(data) {
1507
- return this.messenger.identifyContact(data);
1625
+ return this.identify(data);
1508
1626
  }
1509
1627
 
1510
1628
  async getHelpCollections(options) {
@@ -9492,7 +9610,9 @@
9492
9610
 
9493
9611
  this._unsubscribe = this.state.subscribe((type, data) => {
9494
9612
  if (type === 'connectionChange') {
9495
- const banner = this.element?.querySelector('.messenger-connection-banner');
9613
+ const banner = this.element?.querySelector(
9614
+ '.messenger-connection-banner'
9615
+ );
9496
9616
  if (banner) {
9497
9617
  banner.style.display = data.connected ? 'none' : 'flex';
9498
9618
  }
@@ -9542,9 +9662,10 @@
9542
9662
  ? this._renderEmptyState(isNewConversation)
9543
9663
  : this._renderGroupedMessages(messages);
9544
9664
 
9545
- const defaultPlaceholder = this.options.composePlaceholder || 'Write a message...';
9665
+ const defaultPlaceholder =
9666
+ this.options.composePlaceholder || 'Write a message...';
9546
9667
  const placeholder = isNewConversation
9547
- ? (this.options.composePlaceholder || 'Start typing your message...')
9668
+ ? this.options.composePlaceholder || 'Start typing your message...'
9548
9669
  : isClosed
9549
9670
  ? 'Conversation closed'
9550
9671
  : defaultPlaceholder;
@@ -9565,7 +9686,7 @@
9565
9686
  </div>
9566
9687
  <div class="messenger-chat-header-info">
9567
9688
  <span class="messenger-chat-title">${this._escapeHtml(teamName)}</span>
9568
- <span class="messenger-chat-subtitle">${isClosed ? 'Conversation resolved' : (this.state.responseTime || 'Typically replies within minutes')}</span>
9689
+ <span class="messenger-chat-subtitle">${isClosed ? 'Conversation resolved' : this.state.responseTime || 'Typically replies within minutes'}</span>
9569
9690
  </div>
9570
9691
  <div class="messenger-chat-header-actions">
9571
9692
  <button class="sdk-btn-icon sdk-close-btn messenger-mobile-close-btn" aria-label="Close">
@@ -9682,7 +9803,9 @@
9682
9803
  const messageClass = isOwn
9683
9804
  ? 'messenger-message-own'
9684
9805
  : 'messenger-message-received';
9685
- const timeStr = isLastInGroup ? this._formatMessageTime(message.timestamp) : '';
9806
+ const timeStr = isLastInGroup
9807
+ ? this._formatMessageTime(message.timestamp)
9808
+ : '';
9686
9809
  const attachmentsHtml = this._renderMessageAttachments(message.attachments);
9687
9810
  const isOptimistic = message.isOptimistic;
9688
9811
 
@@ -9696,9 +9819,10 @@
9696
9819
  if (isOwn) {
9697
9820
  const sentIndicator = isLastInGroup
9698
9821
  ? `<div class="messenger-message-meta messenger-message-meta-own">
9699
- ${isOptimistic
9700
- ? `<span class="messenger-message-sent-status">Sending…</span>`
9701
- : `<span class="messenger-message-sent-status">Sent</span>`
9822
+ ${
9823
+ isOptimistic
9824
+ ? `<span class="messenger-message-sent-status">Sending…</span>`
9825
+ : `<span class="messenger-message-sent-status">Sent</span>`
9702
9826
  }
9703
9827
  ${timeStr ? `<span>·</span><span>${timeStr}</span>` : ''}
9704
9828
  </div>`
@@ -10929,7 +11053,8 @@
10929
11053
  const title = conversation.title || teamName;
10930
11054
  const timeAgo = this._formatTimeAgo(conversation.lastMessageTime);
10931
11055
  const preview = conversation.lastMessage
10932
- ? conversation.lastMessage.substring(0, 48) + (conversation.lastMessage.length > 48 ? '...' : '')
11056
+ ? conversation.lastMessage.substring(0, 48) +
11057
+ (conversation.lastMessage.length > 48 ? '...' : '')
10933
11058
  : '';
10934
11059
  const hasUnread = conversation.unread > 0;
10935
11060
 
@@ -11045,7 +11170,9 @@
11045
11170
  }
11046
11171
 
11047
11172
  _attachEvents() {
11048
- const recentCard = this.element.querySelector('.messenger-home-recent-card');
11173
+ const recentCard = this.element.querySelector(
11174
+ '.messenger-home-recent-card'
11175
+ );
11049
11176
  if (recentCard) {
11050
11177
  recentCard.addEventListener('click', () => {
11051
11178
  const convId = recentCard.dataset.conversationId;
@@ -11326,6 +11453,10 @@
11326
11453
  this._handleConversationClosed = this._handleConversationClosed.bind(this);
11327
11454
  }
11328
11455
 
11456
+ _hasTrigger() {
11457
+ return this.options.trigger === true || this.options.trigger === undefined;
11458
+ }
11459
+
11329
11460
  _createInternalFeedbackWidget() {
11330
11461
  try {
11331
11462
  const widget = this.sdk.createWidget('button', {
@@ -11361,11 +11492,13 @@
11361
11492
  this._feedbackWidget = this._createInternalFeedbackWidget();
11362
11493
  }
11363
11494
 
11364
- this.launcher = new MessengerLauncher(this.messengerState, {
11365
- position: this.messengerOptions.position,
11366
- primaryColor: this.messengerOptions.primaryColor,
11367
- });
11368
- container.appendChild(this.launcher.render());
11495
+ if (this._hasTrigger()) {
11496
+ this.launcher = new MessengerLauncher(this.messengerState, {
11497
+ position: this.messengerOptions.position,
11498
+ primaryColor: this.messengerOptions.primaryColor,
11499
+ });
11500
+ container.appendChild(this.launcher.render());
11501
+ }
11369
11502
 
11370
11503
  this.panel = new MessengerPanel(this.messengerState, {
11371
11504
  position: this.messengerOptions.position,
@@ -11500,28 +11633,13 @@
11500
11633
 
11501
11634
  async _handleIdentifyContact(contactData) {
11502
11635
  try {
11503
- const response = await this.apiService.identifyContact({
11504
- name: contactData.name,
11636
+ // Route through sdk.identify() so the SDK-level identity state is updated
11637
+ // and applyIdentity() handles the messenger state + WebSocket as a side effect.
11638
+ const result = await this.sdk.identify({
11505
11639
  email: contactData.email,
11640
+ name: contactData.name,
11506
11641
  });
11507
-
11508
- if (response.status) {
11509
- console.log(
11510
- '[MessengerWidget] Contact identified:',
11511
- response.data.contact_id
11512
- );
11513
- this.messengerState.setIdentified(true, {
11514
- name: contactData.name,
11515
- email: contactData.email,
11516
- });
11517
-
11518
- // Start WebSocket now that session token is available
11519
- if (this.apiService?.sessionToken && !this.wsService?.isConnected) {
11520
- this._initWebSocket();
11521
- }
11522
- }
11523
-
11524
- return response;
11642
+ return result;
11525
11643
  } catch (error) {
11526
11644
  console.error('[MessengerWidget] Failed to identify contact:', error);
11527
11645
  throw error;
@@ -11529,10 +11647,14 @@
11529
11647
  }
11530
11648
 
11531
11649
  markAsIdentified(name, email) {
11532
- this.messengerState.setIdentified(true, { name, email });
11533
- console.log('[MessengerWidget] Marked as identified:', email);
11650
+ // Called externally by the app when the user is already known.
11651
+ // No API call needed — identity was already established via sdk.identify().
11652
+ this.applyIdentity({ name, email });
11653
+ }
11654
+
11655
+ applyIdentity(metadata = {}) {
11656
+ this.messengerState.setIdentified(true, metadata);
11534
11657
 
11535
- // Start WebSocket now that we have a session token
11536
11658
  if (this.apiService?.sessionToken && !this.wsService?.isConnected) {
11537
11659
  this._initWebSocket();
11538
11660
  }
@@ -12076,7 +12198,10 @@
12076
12198
  }
12077
12199
 
12078
12200
  _hasExplicitOption(key) {
12079
- return Object.prototype.hasOwnProperty.call(this._explicitOptions || {}, key);
12201
+ return Object.prototype.hasOwnProperty.call(
12202
+ this._explicitOptions || {},
12203
+ key
12204
+ );
12080
12205
  }
12081
12206
 
12082
12207
  async checkAgentAvailability() {
@@ -12371,6 +12496,7 @@
12371
12496
 
12372
12497
  SurveyWidget.removeDanglingElements();
12373
12498
  this._renderSurvey();
12499
+ this.state.isOpen = true;
12374
12500
  this.surveyState.isVisible = true;
12375
12501
  this.sdk.eventBus.emit('survey:shown', {
12376
12502
  widget: this,
@@ -12380,6 +12506,7 @@
12380
12506
  }
12381
12507
 
12382
12508
  hide() {
12509
+ this.state.isOpen = false;
12383
12510
  this._closeSurvey();
12384
12511
  return this;
12385
12512
  }
@@ -12454,6 +12581,7 @@
12454
12581
  this._attachSurveyEvents();
12455
12582
 
12456
12583
  requestAnimationFrame(() => {
12584
+ if (!this.surveyElement) return;
12457
12585
  this.surveyElement.style.opacity = '1';
12458
12586
  this.surveyElement.style.transform =
12459
12587
  this.surveyOptions.position === 'center'
@@ -13591,6 +13719,7 @@
13591
13719
  }
13592
13720
 
13593
13721
  _closeSurvey(resetState = true, immediate = false) {
13722
+ this.state.isOpen = false;
13594
13723
  if (this._escapeHandler) {
13595
13724
  document.removeEventListener('keydown', this._escapeHandler);
13596
13725
  this._escapeHandler = null;
@@ -13653,6 +13782,22 @@
13653
13782
  this.sdk.eventBus.emit('survey:closed', { widget: this });
13654
13783
  }
13655
13784
 
13785
+ open() {
13786
+ return this.show();
13787
+ }
13788
+
13789
+ close() {
13790
+ return this.hide();
13791
+ }
13792
+
13793
+ toggle() {
13794
+ if (this.surveyState.isVisible) {
13795
+ return this.hide();
13796
+ }
13797
+
13798
+ return this.show();
13799
+ }
13800
+
13656
13801
  destroy() {
13657
13802
  this._closeSurvey(true, true);
13658
13803
  super.destroy();
@@ -13770,6 +13915,7 @@
13770
13915
  constructor(config = {}) {
13771
13916
  this.config = this._validateAndMergeConfig(config);
13772
13917
  this.initialized = false;
13918
+ this.identified = false;
13773
13919
  this.widgets = new Map();
13774
13920
  this.eventBus = new EventBus();
13775
13921
 
@@ -13814,13 +13960,14 @@
13814
13960
  this._injectStyles();
13815
13961
 
13816
13962
  try {
13817
- const initData = await this.apiService.init(this.config.metadata);
13963
+ const initData = await this.apiService.init();
13818
13964
 
13819
13965
  if (initData.config) {
13820
13966
  this.config = deepMerge(initData.config, this.config);
13821
13967
  }
13822
13968
 
13823
13969
  this.initialized = true;
13970
+ const identifyResult = await this._syncConfiguredMetadataAfterInit();
13824
13971
  this.eventBus.emit('sdk:initialized', {
13825
13972
  config: this.config,
13826
13973
  sessionToken: initData.sessionToken,
@@ -13831,6 +13978,7 @@
13831
13978
  config: initData.config || {},
13832
13979
  sessionToken: initData.sessionToken,
13833
13980
  expiresIn: initData.expiresIn,
13981
+ identified: Boolean(identifyResult?.identified),
13834
13982
  };
13835
13983
  } catch (error) {
13836
13984
  this.eventBus.emit('sdk:error', { error });
@@ -13845,10 +13993,14 @@
13845
13993
  );
13846
13994
  }
13847
13995
 
13996
+ const requestedType = type || 'button';
13997
+ const normalizedType = this._normalizeWidgetType(requestedType);
13848
13998
  const widgetId = generateId('widget');
13849
- const widgetConfig = this._getWidgetTypeConfig(type);
13850
- const explicitOptions = this._omitUndefined(options);
13851
- const widgetEnabled = this._isWidgetEnabled(type, {
13999
+ const widgetConfig = this._getWidgetTypeConfig(normalizedType);
14000
+ const explicitOptions = this._omitUndefined(
14001
+ this._normalizeWidgetOptions(normalizedType, options)
14002
+ );
14003
+ const widgetEnabled = this._isWidgetEnabled(normalizedType, {
13852
14004
  ...widgetConfig,
13853
14005
  ...explicitOptions,
13854
14006
  });
@@ -13863,15 +14015,35 @@
13863
14015
  };
13864
14016
 
13865
14017
  try {
13866
- const widget = WidgetFactory.create(type, widgetOptions);
14018
+ const widget = WidgetFactory.create(normalizedType, widgetOptions);
13867
14019
  this.widgets.set(widgetId, widget);
13868
- this.eventBus.emit('widget:created', { widget, type });
14020
+ this.eventBus.emit('widget:created', {
14021
+ widget,
14022
+ type: requestedType,
14023
+ internalType: normalizedType,
14024
+ });
13869
14025
  return widget;
13870
14026
  } catch (error) {
13871
14027
  throw new SDKError(`Failed to create widget: ${error.message}`, error);
13872
14028
  }
13873
14029
  }
13874
14030
 
14031
+ createFeedbackWidget(options = {}) {
14032
+ return this.createWidget('feedback', options);
14033
+ }
14034
+
14035
+ createMessengerWidget(options = {}) {
14036
+ return this.createWidget('messenger', options);
14037
+ }
14038
+
14039
+ createChangelogWidget(options = {}) {
14040
+ return this.createWidget('changelog', options);
14041
+ }
14042
+
14043
+ createSurveyWidget(options = {}) {
14044
+ return this.createWidget('survey', options);
14045
+ }
14046
+
13875
14047
  getWidget(id) {
13876
14048
  return this.widgets.get(id);
13877
14049
  }
@@ -13990,7 +14162,7 @@
13990
14162
  return null;
13991
14163
  }
13992
14164
 
13993
- const surveyWidget = this.createWidget('survey', {
14165
+ const surveyWidget = this.createSurveyWidget({
13994
14166
  surveyId: normalizedOptions.surveyId,
13995
14167
  surveyType:
13996
14168
  normalizedOptions.surveyType || normalizedOptions.type || 'nps',
@@ -14224,18 +14396,34 @@
14224
14396
  ? this.config.widgets
14225
14397
  : {};
14226
14398
 
14227
- const legacyTypeConfig = this._isPlainObject(this.config?.[type])
14228
- ? this.config[type]
14229
- : {};
14399
+ const mergedTypeConfig = this._getWidgetTypeAliases(type).reduce(
14400
+ (config, alias) => {
14401
+ const legacyTypeConfig = this._isPlainObject(this.config?.[alias])
14402
+ ? this.config[alias]
14403
+ : {};
14230
14404
 
14231
- const namespacedTypeConfig = this._isPlainObject(widgetsConfig?.[type])
14232
- ? widgetsConfig[type]
14233
- : {};
14405
+ const namespacedTypeConfig = this._isPlainObject(widgetsConfig?.[alias])
14406
+ ? widgetsConfig[alias]
14407
+ : {};
14234
14408
 
14235
- const mergedTypeConfig = deepMerge(legacyTypeConfig, namespacedTypeConfig);
14409
+ return deepMerge(
14410
+ config,
14411
+ deepMerge(legacyTypeConfig, namespacedTypeConfig)
14412
+ );
14413
+ },
14414
+ {}
14415
+ );
14236
14416
  return this._toCamelCaseObject(mergedTypeConfig);
14237
14417
  }
14238
14418
 
14419
+ _getWidgetTypeAliases(type) {
14420
+ if (type === 'button') {
14421
+ return ['button', 'feedback'];
14422
+ }
14423
+
14424
+ return [type];
14425
+ }
14426
+
14239
14427
  _isWidgetEnabled(type, options = {}) {
14240
14428
  const typeConfig = this._getWidgetTypeConfig(type);
14241
14429
  if (typeConfig.enabled === false) {
@@ -14278,6 +14466,31 @@
14278
14466
  return normalized;
14279
14467
  }
14280
14468
 
14469
+ _normalizeWidgetType(type) {
14470
+ if (type === 'feedback') {
14471
+ return 'button';
14472
+ }
14473
+
14474
+ return type;
14475
+ }
14476
+
14477
+ _normalizeWidgetOptions(type, options = {}) {
14478
+ if (!this._isPlainObject(options)) {
14479
+ return options;
14480
+ }
14481
+
14482
+ const normalizedOptions = { ...options };
14483
+ if (
14484
+ normalizedOptions.headless === true &&
14485
+ normalizedOptions.trigger === undefined &&
14486
+ type !== 'survey'
14487
+ ) {
14488
+ normalizedOptions.trigger = false;
14489
+ }
14490
+
14491
+ return normalizedOptions;
14492
+ }
14493
+
14281
14494
  _omitUndefined(value) {
14282
14495
  if (!this._isPlainObject(value)) {
14283
14496
  return value;
@@ -14322,7 +14535,7 @@
14322
14535
  return null;
14323
14536
  }
14324
14537
 
14325
- const changelogWidget = this.createWidget('changelog', {
14538
+ const changelogWidget = this.createChangelogWidget({
14326
14539
  ...defaults,
14327
14540
  ...configDefaults,
14328
14541
  ...explicitOptions,
@@ -14389,10 +14602,15 @@
14389
14602
  }
14390
14603
 
14391
14604
  setMetadata(metadata) {
14605
+ if (metadata) {
14606
+ this._validateMetadata(metadata);
14607
+ }
14608
+
14392
14609
  this.config.metadata = metadata;
14393
14610
  if (this.apiService) {
14394
14611
  this.apiService.setMetadata(metadata);
14395
14612
  }
14613
+ this.identified = false;
14396
14614
  this.eventBus.emit('metadata:updated', { metadata });
14397
14615
  }
14398
14616
 
@@ -14403,11 +14621,53 @@
14403
14621
  );
14404
14622
  }
14405
14623
 
14624
+ async identify(metadata = this.config.metadata) {
14625
+ if (!this.initialized) {
14626
+ throw new SDKError(
14627
+ 'SDK must be initialized before identifying users. Call init() first.'
14628
+ );
14629
+ }
14630
+
14631
+ if (!metadata) {
14632
+ throw new SDKError(
14633
+ 'Identify requires metadata. Provide at least user_id or email.'
14634
+ );
14635
+ }
14636
+
14637
+ this._validateMetadata(metadata);
14638
+ this.setMetadata(metadata);
14639
+
14640
+ try {
14641
+ const response = await this.apiService.identify(metadata);
14642
+ const configPatch = this._extractIdentifyConfig(response);
14643
+ if (Object.keys(configPatch).length > 0) {
14644
+ this.updateConfig(configPatch);
14645
+ }
14646
+
14647
+ this.identified = true;
14648
+ this._applyIdentityToWidgets(metadata);
14649
+
14650
+ const result = {
14651
+ identified: true,
14652
+ metadata: this.getMetadata(),
14653
+ response,
14654
+ };
14655
+
14656
+ this.eventBus.emit('sdk:identified', result);
14657
+ return result;
14658
+ } catch (error) {
14659
+ this.identified = false;
14660
+ this.eventBus.emit('sdk:error', { error, phase: 'identify' });
14661
+ throw new SDKError(`Failed to identify user: ${error.message}`, error);
14662
+ }
14663
+ }
14664
+
14406
14665
  async reinitialize(newMetadata = null) {
14407
14666
  this.apiService.clearSession();
14408
14667
  this.initialized = false;
14668
+ this.identified = false;
14409
14669
 
14410
- if (newMetadata) {
14670
+ if (newMetadata !== null) {
14411
14671
  this.setMetadata(newMetadata);
14412
14672
  }
14413
14673
 
@@ -14436,10 +14696,11 @@
14436
14696
 
14437
14697
  destroy() {
14438
14698
  this.destroyAllWidgets();
14439
- this.eventBus.removeAllListeners();
14699
+ this.eventBus.emit('sdk:destroyed');
14700
+ this.eventBus.clear();
14440
14701
  this.apiService.clearSession();
14441
14702
  this.initialized = false;
14442
- this.eventBus.emit('sdk:destroyed');
14703
+ this.identified = false;
14443
14704
  }
14444
14705
 
14445
14706
  hasFeedbackBeenSubmitted(cooldownDays = 30) {
@@ -14558,7 +14819,12 @@
14558
14819
 
14559
14820
  _bindMethods() {
14560
14821
  this.createWidget = this.createWidget.bind(this);
14822
+ this.createFeedbackWidget = this.createFeedbackWidget.bind(this);
14823
+ this.createMessengerWidget = this.createMessengerWidget.bind(this);
14824
+ this.createChangelogWidget = this.createChangelogWidget.bind(this);
14825
+ this.createSurveyWidget = this.createSurveyWidget.bind(this);
14561
14826
  this.destroyWidget = this.destroyWidget.bind(this);
14827
+ this.identify = this.identify.bind(this);
14562
14828
  this.updateConfig = this.updateConfig.bind(this);
14563
14829
  }
14564
14830
 
@@ -14594,6 +14860,49 @@
14594
14860
  : undefined,
14595
14861
  };
14596
14862
  }
14863
+
14864
+ async _syncConfiguredMetadataAfterInit() {
14865
+ if (
14866
+ !this.config.metadata ||
14867
+ this.apiService.identitySyncedToken === this.apiService.sessionToken
14868
+ ) {
14869
+ return null;
14870
+ }
14871
+
14872
+ try {
14873
+ return await this.identify(this.config.metadata);
14874
+ } catch (error) {
14875
+ if (this.config.debug) {
14876
+ console.warn('[Product7] Initial identify failed:', error);
14877
+ }
14878
+ return null;
14879
+ }
14880
+ }
14881
+
14882
+ _extractIdentifyConfig(response) {
14883
+ const payload = this._isPlainObject(response?.data)
14884
+ ? response.data
14885
+ : response || {};
14886
+ const configPatch = this._isPlainObject(payload.config)
14887
+ ? payload.config
14888
+ : {};
14889
+
14890
+ if (payload.last_feedback_at !== undefined) {
14891
+ configPatch.last_feedback_at = payload.last_feedback_at;
14892
+ } else if (payload.lastFeedbackAt !== undefined) {
14893
+ configPatch.last_feedback_at = payload.lastFeedbackAt;
14894
+ }
14895
+
14896
+ return this._omitUndefined(configPatch);
14897
+ }
14898
+
14899
+ _applyIdentityToWidgets(metadata) {
14900
+ for (const widget of this.widgets.values()) {
14901
+ if (typeof widget.applyIdentity === 'function') {
14902
+ widget.applyIdentity(metadata);
14903
+ }
14904
+ }
14905
+ }
14597
14906
  };
14598
14907
 
14599
14908
  // --- Identify: transform flat user data into internal format ---
@@ -14676,9 +14985,21 @@
14676
14985
  return obj;
14677
14986
  }
14678
14987
 
14988
+ function hasIdentifyMetadata(metadata = {}) {
14989
+ return Object.values(metadata).some((value) => value !== undefined);
14990
+ }
14991
+
14679
14992
  // --- Ensure SDK is initialized (shared by widget inits) ---
14680
14993
 
14681
14994
  async function ensureSDK(options) {
14995
+ if (
14996
+ Product7._sdk &&
14997
+ options.organization &&
14998
+ Product7._sdk.config.workspace !== options.organization
14999
+ ) {
15000
+ Product7.destroy();
15001
+ }
15002
+
14682
15003
  if (Product7._sdk) return Product7._sdk;
14683
15004
 
14684
15005
  if (options.organization) {
@@ -14721,63 +15042,58 @@
14721
15042
  async identify(data = {}, callback) {
14722
15043
  try {
14723
15044
  const transformed = transformIdentifyData(data);
14724
- Product7._organization = transformed.workspace;
15045
+ Product7._organization =
15046
+ transformed.workspace || Product7._organization || null;
14725
15047
 
14726
15048
  const config = cleanUndefined({
14727
- workspace: transformed.workspace,
14728
- metadata: transformed.metadata,
15049
+ workspace: Product7._organization,
14729
15050
  debug: transformed.debug,
14730
15051
  mock: transformed.mock,
14731
15052
  env: transformed.env,
14732
15053
  apiUrl: transformed.apiUrl,
14733
15054
  });
14734
15055
 
14735
- const sdk = new Product7$1(config);
14736
- const initData = await sdk.init();
15056
+ let sdk = Product7._sdk;
15057
+ const requiresNewSDK =
15058
+ !sdk || (config.workspace && sdk.config.workspace !== config.workspace);
14737
15059
 
14738
- Product7._sdk = sdk;
14739
- Product7._identified = true;
14740
-
14741
- // Sync custom attributes + segments via /widget/identify
14742
- if (transformed.metadata && sdk.apiService?.sessionToken) {
14743
- try {
14744
- const identifyPayload = {
14745
- user_id: transformed.metadata.user_id,
14746
- email: transformed.metadata.email,
14747
- name: transformed.metadata.name,
14748
- avatar: transformed.metadata.profile_picture,
14749
- attributes: transformed.metadata.custom_fields || {},
14750
- };
14751
- if (transformed.metadata.company) {
14752
- identifyPayload.company = transformed.metadata.company;
14753
- }
14754
- await sdk.apiService._makeRequest('/widget/identify', {
14755
- method: 'POST',
14756
- body: JSON.stringify(identifyPayload),
14757
- headers: {
14758
- 'Content-Type': 'application/json',
14759
- Authorization: `Bearer ${sdk.apiService.sessionToken}`,
14760
- },
14761
- });
14762
- } catch (identifyErr) {
14763
- if (config.debug) {
14764
- console.warn('[Product7] Attribute sync failed:', identifyErr);
14765
- }
15060
+ if (requiresNewSDK) {
15061
+ if (Product7._sdk) {
15062
+ Product7.destroy();
15063
+ Product7._organization = config.workspace || null;
14766
15064
  }
15065
+ sdk = new Product7$1(config);
15066
+ Product7._sdk = sdk;
14767
15067
  }
14768
15068
 
15069
+ const initData = sdk.initialized
15070
+ ? {
15071
+ alreadyInitialized: true,
15072
+ sessionToken: sdk.apiService?.sessionToken,
15073
+ }
15074
+ : await sdk.init();
15075
+
15076
+ const identifyData = hasIdentifyMetadata(transformed.metadata)
15077
+ ? await sdk.identify(transformed.metadata)
15078
+ : null;
15079
+ Product7._identified = Boolean(identifyData?.identified);
15080
+
14769
15081
  Product7._flushQueue();
14770
15082
 
14771
15083
  if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
14772
15084
  window.dispatchEvent(
14773
15085
  new CustomEvent('Product7Ready', {
14774
- detail: { sdk, config, initData },
15086
+ detail: { sdk, config: sdk.config, initData, identifyData },
14775
15087
  })
14776
15088
  );
14777
15089
  }
14778
15090
 
14779
15091
  if (typeof callback === 'function') callback(null);
14780
- return initData;
15092
+ return {
15093
+ ...initData,
15094
+ identified: Product7._identified,
15095
+ identify: identifyData,
15096
+ };
14781
15097
  } catch (error) {
14782
15098
  console.error('[Product7] Identify failed:', error);
14783
15099
 
@@ -14836,7 +15152,7 @@
14836
15152
  if (!options.placement) {
14837
15153
  widgetOptions.autoShow = false;
14838
15154
  widgetOptions.displayMode = 'modal';
14839
- widgetOptions._noTriggerButton = true;
15155
+ widgetOptions.headless = true;
14840
15156
  } else {
14841
15157
  // Trigger button is always visible when placement is set
14842
15158
  widgetOptions.suppressAfterSubmission = false;
@@ -14844,7 +15160,7 @@
14844
15160
  }
14845
15161
 
14846
15162
  try {
14847
- const widget = sdk.createWidget('button', widgetOptions);
15163
+ const widget = sdk.createFeedbackWidget(widgetOptions);
14848
15164
  widget.mount();
14849
15165
 
14850
15166
  if (options.placement) {
@@ -14883,13 +15199,13 @@
14883
15199
  if (options.setBoard) {
14884
15200
  Product7._feedbackWidget.options.boardName = options.setBoard;
14885
15201
  }
14886
- Product7._feedbackWidget.openPanel();
15202
+ Product7._feedbackWidget.open();
14887
15203
  }
14888
15204
  },
14889
15205
 
14890
15206
  closeFeedback() {
14891
15207
  if (Product7._feedbackWidget) {
14892
- Product7._feedbackWidget.closePanel();
15208
+ Product7._feedbackWidget.close();
14893
15209
  }
14894
15210
  },
14895
15211
 
@@ -14948,7 +15264,7 @@
14948
15264
  widgetOptions.enabled = true;
14949
15265
 
14950
15266
  try {
14951
- const widget = sdk.createWidget('messenger', widgetOptions);
15267
+ const widget = sdk.createMessengerWidget(widgetOptions);
14952
15268
  widget.mount();
14953
15269
  widget.show();
14954
15270