@product7/product7-js 0.3.2 → 0.3.5

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,8 +1205,13 @@
1186
1205
  clearSession() {
1187
1206
  this.sessionToken = null;
1188
1207
  this.sessionExpiry = null;
1208
+ this.identitySyncedToken = null;
1209
+ this.contactId = null;
1210
+ this.contactEmail = null;
1211
+ this.contactName = null;
1189
1212
  this._removeData('product7_session');
1190
1213
  this._removeData('product7_metadata');
1214
+ this._removeData('product7_contact');
1191
1215
  }
1192
1216
 
1193
1217
  _storeSession() {
@@ -1211,6 +1235,14 @@
1211
1235
  if (!stored) return false;
1212
1236
 
1213
1237
  const sessionData = JSON.parse(stored);
1238
+ if (
1239
+ this.workspace &&
1240
+ sessionData.workspace &&
1241
+ sessionData.workspace !== this.workspace
1242
+ ) {
1243
+ localStorage.removeItem('product7_session');
1244
+ return false;
1245
+ }
1214
1246
  this.sessionToken = sessionData.token;
1215
1247
  this.sessionExpiry = new Date(sessionData.expiry);
1216
1248
 
@@ -1220,6 +1252,32 @@
1220
1252
  }
1221
1253
  }
1222
1254
 
1255
+ _loadStoredMetadata() {
1256
+ if (this.metadata || typeof localStorage === 'undefined') return false;
1257
+
1258
+ try {
1259
+ const session = localStorage.getItem('product7_session');
1260
+ if (session) {
1261
+ const sessionData = JSON.parse(session);
1262
+ if (
1263
+ this.workspace &&
1264
+ sessionData.workspace &&
1265
+ sessionData.workspace !== this.workspace
1266
+ ) {
1267
+ return false;
1268
+ }
1269
+ }
1270
+
1271
+ const stored = localStorage.getItem('product7_metadata');
1272
+ if (!stored) return false;
1273
+
1274
+ this.metadata = JSON.parse(stored);
1275
+ return true;
1276
+ } catch (error) {
1277
+ return false;
1278
+ }
1279
+ }
1280
+
1223
1281
  _storeData(key, value) {
1224
1282
  if (typeof localStorage !== 'undefined') {
1225
1283
  localStorage.setItem(key, JSON.stringify(value));
@@ -1282,6 +1340,36 @@
1282
1340
  const queryString = this._buildQueryParams(params);
1283
1341
  return `${endpoint}${queryString ? '?' + queryString : ''}`;
1284
1342
  }
1343
+
1344
+ _buildIdentifyPayload(metadata = {}) {
1345
+ const payload = {
1346
+ user_id: metadata.user_id,
1347
+ email: metadata.email,
1348
+ name: metadata.name,
1349
+ avatar:
1350
+ metadata.profile_picture || metadata.avatar_url || metadata.avatar,
1351
+ attributes: metadata.custom_fields || {},
1352
+ };
1353
+
1354
+ if (metadata.company) {
1355
+ payload.company = metadata.company;
1356
+ }
1357
+
1358
+ return payload;
1359
+ }
1360
+
1361
+ async _restoreIdentity() {
1362
+ if (
1363
+ !this.metadata ||
1364
+ !this.sessionToken ||
1365
+ !this.identitySyncedToken ||
1366
+ this.identitySyncedToken === this.sessionToken
1367
+ ) {
1368
+ return;
1369
+ }
1370
+
1371
+ await this.identify(this.metadata);
1372
+ }
1285
1373
  }
1286
1374
 
1287
1375
  class APIService extends BaseAPIService {
@@ -1503,8 +1591,94 @@
1503
1591
  return this.messenger.submitRating(conversationId, data);
1504
1592
  }
1505
1593
 
1594
+ async identify(metadata) {
1595
+ await this._ensureSession();
1596
+
1597
+ if (this.mock) {
1598
+ await new Promise((r) => setTimeout(r, 300));
1599
+ const mockResponse = {
1600
+ status: true,
1601
+ data: {
1602
+ contact_id: 'mock_contact_' + Date.now(),
1603
+ email: metadata.email,
1604
+ name: metadata.name || '',
1605
+ is_new: true,
1606
+ },
1607
+ };
1608
+ this._storeContactIdentity(mockResponse.data, metadata);
1609
+ return mockResponse;
1610
+ }
1611
+
1612
+ const response = await this._makeRequest('/widget/messenger/identify', {
1613
+ method: 'POST',
1614
+ headers: {
1615
+ 'Content-Type': 'application/json',
1616
+ Authorization: `Bearer ${this.sessionToken}`,
1617
+ },
1618
+ body: JSON.stringify({
1619
+ user_id: metadata.user_id || null,
1620
+ email: metadata.email || '',
1621
+ name: metadata.name || '',
1622
+ phone: metadata.phone || '',
1623
+ company: metadata.company || '',
1624
+ avatar_url: metadata.avatar_url || '',
1625
+ metadata: metadata.custom_fields || {},
1626
+ }),
1627
+ });
1628
+
1629
+ if (response?.status && response?.data) {
1630
+ this._storeContactIdentity(response.data, metadata);
1631
+ }
1632
+
1633
+ return response;
1634
+ }
1635
+
1636
+ _storeContactIdentity(data, metadata = {}) {
1637
+ this.contactId = data.contact_id || null;
1638
+ this.contactEmail = data.email || metadata.email || null;
1639
+ this.contactName = data.name || metadata.name || null;
1640
+
1641
+ try {
1642
+ localStorage.setItem(
1643
+ 'product7_contact',
1644
+ JSON.stringify({
1645
+ contactId: this.contactId,
1646
+ contactEmail: this.contactEmail,
1647
+ contactName: this.contactName,
1648
+ })
1649
+ );
1650
+ } catch (e) {
1651
+ /* silent */
1652
+ }
1653
+ }
1654
+
1655
+ getContactIdentity() {
1656
+ if (this.contactId) {
1657
+ return {
1658
+ contactId: this.contactId,
1659
+ contactEmail: this.contactEmail,
1660
+ contactName: this.contactName,
1661
+ };
1662
+ }
1663
+
1664
+ try {
1665
+ const stored = localStorage.getItem('product7_contact');
1666
+ if (stored) {
1667
+ const parsed = JSON.parse(stored);
1668
+ this.contactId = parsed.contactId;
1669
+ this.contactEmail = parsed.contactEmail;
1670
+ this.contactName = parsed.contactName;
1671
+ return parsed;
1672
+ }
1673
+ } catch (e) {
1674
+ /* silent */
1675
+ }
1676
+
1677
+ return null;
1678
+ }
1679
+
1506
1680
  async identifyContact(data) {
1507
- return this.messenger.identifyContact(data);
1681
+ return this.identify(data);
1508
1682
  }
1509
1683
 
1510
1684
  async getHelpCollections(options) {
@@ -9492,7 +9666,9 @@
9492
9666
 
9493
9667
  this._unsubscribe = this.state.subscribe((type, data) => {
9494
9668
  if (type === 'connectionChange') {
9495
- const banner = this.element?.querySelector('.messenger-connection-banner');
9669
+ const banner = this.element?.querySelector(
9670
+ '.messenger-connection-banner'
9671
+ );
9496
9672
  if (banner) {
9497
9673
  banner.style.display = data.connected ? 'none' : 'flex';
9498
9674
  }
@@ -9542,9 +9718,10 @@
9542
9718
  ? this._renderEmptyState(isNewConversation)
9543
9719
  : this._renderGroupedMessages(messages);
9544
9720
 
9545
- const defaultPlaceholder = this.options.composePlaceholder || 'Write a message...';
9721
+ const defaultPlaceholder =
9722
+ this.options.composePlaceholder || 'Write a message...';
9546
9723
  const placeholder = isNewConversation
9547
- ? (this.options.composePlaceholder || 'Start typing your message...')
9724
+ ? this.options.composePlaceholder || 'Start typing your message...'
9548
9725
  : isClosed
9549
9726
  ? 'Conversation closed'
9550
9727
  : defaultPlaceholder;
@@ -9565,7 +9742,7 @@
9565
9742
  </div>
9566
9743
  <div class="messenger-chat-header-info">
9567
9744
  <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>
9745
+ <span class="messenger-chat-subtitle">${isClosed ? 'Conversation resolved' : this.state.responseTime || 'Typically replies within minutes'}</span>
9569
9746
  </div>
9570
9747
  <div class="messenger-chat-header-actions">
9571
9748
  <button class="sdk-btn-icon sdk-close-btn messenger-mobile-close-btn" aria-label="Close">
@@ -9682,7 +9859,9 @@
9682
9859
  const messageClass = isOwn
9683
9860
  ? 'messenger-message-own'
9684
9861
  : 'messenger-message-received';
9685
- const timeStr = isLastInGroup ? this._formatMessageTime(message.timestamp) : '';
9862
+ const timeStr = isLastInGroup
9863
+ ? this._formatMessageTime(message.timestamp)
9864
+ : '';
9686
9865
  const attachmentsHtml = this._renderMessageAttachments(message.attachments);
9687
9866
  const isOptimistic = message.isOptimistic;
9688
9867
 
@@ -9696,9 +9875,10 @@
9696
9875
  if (isOwn) {
9697
9876
  const sentIndicator = isLastInGroup
9698
9877
  ? `<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>`
9878
+ ${
9879
+ isOptimistic
9880
+ ? `<span class="messenger-message-sent-status">Sending…</span>`
9881
+ : `<span class="messenger-message-sent-status">Sent</span>`
9702
9882
  }
9703
9883
  ${timeStr ? `<span>·</span><span>${timeStr}</span>` : ''}
9704
9884
  </div>`
@@ -10929,7 +11109,8 @@
10929
11109
  const title = conversation.title || teamName;
10930
11110
  const timeAgo = this._formatTimeAgo(conversation.lastMessageTime);
10931
11111
  const preview = conversation.lastMessage
10932
- ? conversation.lastMessage.substring(0, 48) + (conversation.lastMessage.length > 48 ? '...' : '')
11112
+ ? conversation.lastMessage.substring(0, 48) +
11113
+ (conversation.lastMessage.length > 48 ? '...' : '')
10933
11114
  : '';
10934
11115
  const hasUnread = conversation.unread > 0;
10935
11116
 
@@ -11045,7 +11226,9 @@
11045
11226
  }
11046
11227
 
11047
11228
  _attachEvents() {
11048
- const recentCard = this.element.querySelector('.messenger-home-recent-card');
11229
+ const recentCard = this.element.querySelector(
11230
+ '.messenger-home-recent-card'
11231
+ );
11049
11232
  if (recentCard) {
11050
11233
  recentCard.addEventListener('click', () => {
11051
11234
  const convId = recentCard.dataset.conversationId;
@@ -11284,6 +11467,7 @@
11284
11467
  initialView: options.initialView || 'home',
11285
11468
  previewData: options.previewData || null,
11286
11469
  featuredContent: options.featuredContent || null,
11470
+ feedbackBoardName: options.feedbackBoardName || null,
11287
11471
  feedbackUrl: options.feedbackUrl || null,
11288
11472
  changelogUrl: options.changelogUrl || null,
11289
11473
  helpUrl: options.helpUrl || null,
@@ -11326,12 +11510,17 @@
11326
11510
  this._handleConversationClosed = this._handleConversationClosed.bind(this);
11327
11511
  }
11328
11512
 
11513
+ _hasTrigger() {
11514
+ return this.options.trigger === true || this.options.trigger === undefined;
11515
+ }
11516
+
11329
11517
  _createInternalFeedbackWidget() {
11330
11518
  try {
11331
11519
  const widget = this.sdk.createWidget('button', {
11332
11520
  trigger: false,
11333
11521
  displayMode: 'modal',
11334
- boardName: this.sdk.config.boardName,
11522
+ boardName:
11523
+ this.messengerOptions.feedbackBoardName || this.sdk.config.boardName,
11335
11524
  primaryColor: this.messengerOptions.primaryColor,
11336
11525
  theme: this.messengerOptions.theme,
11337
11526
  });
@@ -11361,11 +11550,13 @@
11361
11550
  this._feedbackWidget = this._createInternalFeedbackWidget();
11362
11551
  }
11363
11552
 
11364
- this.launcher = new MessengerLauncher(this.messengerState, {
11365
- position: this.messengerOptions.position,
11366
- primaryColor: this.messengerOptions.primaryColor,
11367
- });
11368
- container.appendChild(this.launcher.render());
11553
+ if (this._hasTrigger()) {
11554
+ this.launcher = new MessengerLauncher(this.messengerState, {
11555
+ position: this.messengerOptions.position,
11556
+ primaryColor: this.messengerOptions.primaryColor,
11557
+ });
11558
+ container.appendChild(this.launcher.render());
11559
+ }
11369
11560
 
11370
11561
  this.panel = new MessengerPanel(this.messengerState, {
11371
11562
  position: this.messengerOptions.position,
@@ -11500,28 +11691,13 @@
11500
11691
 
11501
11692
  async _handleIdentifyContact(contactData) {
11502
11693
  try {
11503
- const response = await this.apiService.identifyContact({
11504
- name: contactData.name,
11694
+ // Route through sdk.identify() so the SDK-level identity state is updated
11695
+ // and applyIdentity() handles the messenger state + WebSocket as a side effect.
11696
+ const result = await this.sdk.identify({
11505
11697
  email: contactData.email,
11698
+ name: contactData.name,
11506
11699
  });
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;
11700
+ return result;
11525
11701
  } catch (error) {
11526
11702
  console.error('[MessengerWidget] Failed to identify contact:', error);
11527
11703
  throw error;
@@ -11529,10 +11705,14 @@
11529
11705
  }
11530
11706
 
11531
11707
  markAsIdentified(name, email) {
11532
- this.messengerState.setIdentified(true, { name, email });
11533
- console.log('[MessengerWidget] Marked as identified:', email);
11708
+ // Called externally by the app when the user is already known.
11709
+ // No API call needed — identity was already established via sdk.identify().
11710
+ this.applyIdentity({ name, email });
11711
+ }
11712
+
11713
+ applyIdentity(metadata = {}) {
11714
+ this.messengerState.setIdentified(true, metadata);
11534
11715
 
11535
- // Start WebSocket now that we have a session token
11536
11716
  if (this.apiService?.sessionToken && !this.wsService?.isConnected) {
11537
11717
  this._initWebSocket();
11538
11718
  }
@@ -12076,7 +12256,10 @@
12076
12256
  }
12077
12257
 
12078
12258
  _hasExplicitOption(key) {
12079
- return Object.prototype.hasOwnProperty.call(this._explicitOptions || {}, key);
12259
+ return Object.prototype.hasOwnProperty.call(
12260
+ this._explicitOptions || {},
12261
+ key
12262
+ );
12080
12263
  }
12081
12264
 
12082
12265
  async checkAgentAvailability() {
@@ -12371,6 +12554,7 @@
12371
12554
 
12372
12555
  SurveyWidget.removeDanglingElements();
12373
12556
  this._renderSurvey();
12557
+ this.state.isOpen = true;
12374
12558
  this.surveyState.isVisible = true;
12375
12559
  this.sdk.eventBus.emit('survey:shown', {
12376
12560
  widget: this,
@@ -12380,6 +12564,7 @@
12380
12564
  }
12381
12565
 
12382
12566
  hide() {
12567
+ this.state.isOpen = false;
12383
12568
  this._closeSurvey();
12384
12569
  return this;
12385
12570
  }
@@ -12454,6 +12639,7 @@
12454
12639
  this._attachSurveyEvents();
12455
12640
 
12456
12641
  requestAnimationFrame(() => {
12642
+ if (!this.surveyElement) return;
12457
12643
  this.surveyElement.style.opacity = '1';
12458
12644
  this.surveyElement.style.transform =
12459
12645
  this.surveyOptions.position === 'center'
@@ -13591,6 +13777,7 @@
13591
13777
  }
13592
13778
 
13593
13779
  _closeSurvey(resetState = true, immediate = false) {
13780
+ this.state.isOpen = false;
13594
13781
  if (this._escapeHandler) {
13595
13782
  document.removeEventListener('keydown', this._escapeHandler);
13596
13783
  this._escapeHandler = null;
@@ -13653,6 +13840,22 @@
13653
13840
  this.sdk.eventBus.emit('survey:closed', { widget: this });
13654
13841
  }
13655
13842
 
13843
+ open() {
13844
+ return this.show();
13845
+ }
13846
+
13847
+ close() {
13848
+ return this.hide();
13849
+ }
13850
+
13851
+ toggle() {
13852
+ if (this.surveyState.isVisible) {
13853
+ return this.hide();
13854
+ }
13855
+
13856
+ return this.show();
13857
+ }
13858
+
13656
13859
  destroy() {
13657
13860
  this._closeSurvey(true, true);
13658
13861
  super.destroy();
@@ -13770,6 +13973,7 @@
13770
13973
  constructor(config = {}) {
13771
13974
  this.config = this._validateAndMergeConfig(config);
13772
13975
  this.initialized = false;
13976
+ this.identified = false;
13773
13977
  this.widgets = new Map();
13774
13978
  this.eventBus = new EventBus();
13775
13979
 
@@ -13814,13 +14018,14 @@
13814
14018
  this._injectStyles();
13815
14019
 
13816
14020
  try {
13817
- const initData = await this.apiService.init(this.config.metadata);
14021
+ const initData = await this.apiService.init();
13818
14022
 
13819
14023
  if (initData.config) {
13820
14024
  this.config = deepMerge(initData.config, this.config);
13821
14025
  }
13822
14026
 
13823
14027
  this.initialized = true;
14028
+ const identifyResult = await this._syncConfiguredMetadataAfterInit();
13824
14029
  this.eventBus.emit('sdk:initialized', {
13825
14030
  config: this.config,
13826
14031
  sessionToken: initData.sessionToken,
@@ -13831,6 +14036,7 @@
13831
14036
  config: initData.config || {},
13832
14037
  sessionToken: initData.sessionToken,
13833
14038
  expiresIn: initData.expiresIn,
14039
+ identified: Boolean(identifyResult?.identified),
13834
14040
  };
13835
14041
  } catch (error) {
13836
14042
  this.eventBus.emit('sdk:error', { error });
@@ -13845,10 +14051,14 @@
13845
14051
  );
13846
14052
  }
13847
14053
 
14054
+ const requestedType = type || 'button';
14055
+ const normalizedType = this._normalizeWidgetType(requestedType);
13848
14056
  const widgetId = generateId('widget');
13849
- const widgetConfig = this._getWidgetTypeConfig(type);
13850
- const explicitOptions = this._omitUndefined(options);
13851
- const widgetEnabled = this._isWidgetEnabled(type, {
14057
+ const widgetConfig = this._getWidgetTypeConfig(normalizedType);
14058
+ const explicitOptions = this._omitUndefined(
14059
+ this._normalizeWidgetOptions(normalizedType, options)
14060
+ );
14061
+ const widgetEnabled = this._isWidgetEnabled(normalizedType, {
13852
14062
  ...widgetConfig,
13853
14063
  ...explicitOptions,
13854
14064
  });
@@ -13863,15 +14073,35 @@
13863
14073
  };
13864
14074
 
13865
14075
  try {
13866
- const widget = WidgetFactory.create(type, widgetOptions);
14076
+ const widget = WidgetFactory.create(normalizedType, widgetOptions);
13867
14077
  this.widgets.set(widgetId, widget);
13868
- this.eventBus.emit('widget:created', { widget, type });
14078
+ this.eventBus.emit('widget:created', {
14079
+ widget,
14080
+ type: requestedType,
14081
+ internalType: normalizedType,
14082
+ });
13869
14083
  return widget;
13870
14084
  } catch (error) {
13871
14085
  throw new SDKError(`Failed to create widget: ${error.message}`, error);
13872
14086
  }
13873
14087
  }
13874
14088
 
14089
+ createFeedbackWidget(options = {}) {
14090
+ return this.createWidget('feedback', options);
14091
+ }
14092
+
14093
+ createMessengerWidget(options = {}) {
14094
+ return this.createWidget('messenger', options);
14095
+ }
14096
+
14097
+ createChangelogWidget(options = {}) {
14098
+ return this.createWidget('changelog', options);
14099
+ }
14100
+
14101
+ createSurveyWidget(options = {}) {
14102
+ return this.createWidget('survey', options);
14103
+ }
14104
+
13875
14105
  getWidget(id) {
13876
14106
  return this.widgets.get(id);
13877
14107
  }
@@ -13990,7 +14220,7 @@
13990
14220
  return null;
13991
14221
  }
13992
14222
 
13993
- const surveyWidget = this.createWidget('survey', {
14223
+ const surveyWidget = this.createSurveyWidget({
13994
14224
  surveyId: normalizedOptions.surveyId,
13995
14225
  surveyType:
13996
14226
  normalizedOptions.surveyType || normalizedOptions.type || 'nps',
@@ -14224,18 +14454,34 @@
14224
14454
  ? this.config.widgets
14225
14455
  : {};
14226
14456
 
14227
- const legacyTypeConfig = this._isPlainObject(this.config?.[type])
14228
- ? this.config[type]
14229
- : {};
14457
+ const mergedTypeConfig = this._getWidgetTypeAliases(type).reduce(
14458
+ (config, alias) => {
14459
+ const legacyTypeConfig = this._isPlainObject(this.config?.[alias])
14460
+ ? this.config[alias]
14461
+ : {};
14230
14462
 
14231
- const namespacedTypeConfig = this._isPlainObject(widgetsConfig?.[type])
14232
- ? widgetsConfig[type]
14233
- : {};
14463
+ const namespacedTypeConfig = this._isPlainObject(widgetsConfig?.[alias])
14464
+ ? widgetsConfig[alias]
14465
+ : {};
14234
14466
 
14235
- const mergedTypeConfig = deepMerge(legacyTypeConfig, namespacedTypeConfig);
14467
+ return deepMerge(
14468
+ config,
14469
+ deepMerge(legacyTypeConfig, namespacedTypeConfig)
14470
+ );
14471
+ },
14472
+ {}
14473
+ );
14236
14474
  return this._toCamelCaseObject(mergedTypeConfig);
14237
14475
  }
14238
14476
 
14477
+ _getWidgetTypeAliases(type) {
14478
+ if (type === 'button') {
14479
+ return ['button', 'feedback'];
14480
+ }
14481
+
14482
+ return [type];
14483
+ }
14484
+
14239
14485
  _isWidgetEnabled(type, options = {}) {
14240
14486
  const typeConfig = this._getWidgetTypeConfig(type);
14241
14487
  if (typeConfig.enabled === false) {
@@ -14278,6 +14524,31 @@
14278
14524
  return normalized;
14279
14525
  }
14280
14526
 
14527
+ _normalizeWidgetType(type) {
14528
+ if (type === 'feedback') {
14529
+ return 'button';
14530
+ }
14531
+
14532
+ return type;
14533
+ }
14534
+
14535
+ _normalizeWidgetOptions(type, options = {}) {
14536
+ if (!this._isPlainObject(options)) {
14537
+ return options;
14538
+ }
14539
+
14540
+ const normalizedOptions = { ...options };
14541
+ if (
14542
+ normalizedOptions.headless === true &&
14543
+ normalizedOptions.trigger === undefined &&
14544
+ type !== 'survey'
14545
+ ) {
14546
+ normalizedOptions.trigger = false;
14547
+ }
14548
+
14549
+ return normalizedOptions;
14550
+ }
14551
+
14281
14552
  _omitUndefined(value) {
14282
14553
  if (!this._isPlainObject(value)) {
14283
14554
  return value;
@@ -14322,7 +14593,7 @@
14322
14593
  return null;
14323
14594
  }
14324
14595
 
14325
- const changelogWidget = this.createWidget('changelog', {
14596
+ const changelogWidget = this.createChangelogWidget({
14326
14597
  ...defaults,
14327
14598
  ...configDefaults,
14328
14599
  ...explicitOptions,
@@ -14389,10 +14660,15 @@
14389
14660
  }
14390
14661
 
14391
14662
  setMetadata(metadata) {
14663
+ if (metadata) {
14664
+ this._validateMetadata(metadata);
14665
+ }
14666
+
14392
14667
  this.config.metadata = metadata;
14393
14668
  if (this.apiService) {
14394
14669
  this.apiService.setMetadata(metadata);
14395
14670
  }
14671
+ this.identified = false;
14396
14672
  this.eventBus.emit('metadata:updated', { metadata });
14397
14673
  }
14398
14674
 
@@ -14403,11 +14679,53 @@
14403
14679
  );
14404
14680
  }
14405
14681
 
14682
+ async identify(metadata = this.config.metadata) {
14683
+ if (!this.initialized) {
14684
+ throw new SDKError(
14685
+ 'SDK must be initialized before identifying users. Call init() first.'
14686
+ );
14687
+ }
14688
+
14689
+ if (!metadata) {
14690
+ throw new SDKError(
14691
+ 'Identify requires metadata. Provide at least user_id or email.'
14692
+ );
14693
+ }
14694
+
14695
+ this._validateMetadata(metadata);
14696
+ this.setMetadata(metadata);
14697
+
14698
+ try {
14699
+ const response = await this.apiService.identify(metadata);
14700
+ const configPatch = this._extractIdentifyConfig(response);
14701
+ if (Object.keys(configPatch).length > 0) {
14702
+ this.updateConfig(configPatch);
14703
+ }
14704
+
14705
+ this.identified = true;
14706
+ this._applyIdentityToWidgets(metadata);
14707
+
14708
+ const result = {
14709
+ identified: true,
14710
+ metadata: this.getMetadata(),
14711
+ response,
14712
+ };
14713
+
14714
+ this.eventBus.emit('sdk:identified', result);
14715
+ return result;
14716
+ } catch (error) {
14717
+ this.identified = false;
14718
+ this.eventBus.emit('sdk:error', { error, phase: 'identify' });
14719
+ throw new SDKError(`Failed to identify user: ${error.message}`, error);
14720
+ }
14721
+ }
14722
+
14406
14723
  async reinitialize(newMetadata = null) {
14407
14724
  this.apiService.clearSession();
14408
14725
  this.initialized = false;
14726
+ this.identified = false;
14409
14727
 
14410
- if (newMetadata) {
14728
+ if (newMetadata !== null) {
14411
14729
  this.setMetadata(newMetadata);
14412
14730
  }
14413
14731
 
@@ -14436,10 +14754,11 @@
14436
14754
 
14437
14755
  destroy() {
14438
14756
  this.destroyAllWidgets();
14439
- this.eventBus.removeAllListeners();
14757
+ this.eventBus.emit('sdk:destroyed');
14758
+ this.eventBus.clear();
14440
14759
  this.apiService.clearSession();
14441
14760
  this.initialized = false;
14442
- this.eventBus.emit('sdk:destroyed');
14761
+ this.identified = false;
14443
14762
  }
14444
14763
 
14445
14764
  hasFeedbackBeenSubmitted(cooldownDays = 30) {
@@ -14507,7 +14826,7 @@
14507
14826
  metadata: null,
14508
14827
  position: 'right',
14509
14828
  theme: 'light',
14510
- boardName: 'general',
14829
+ boardName: 'feature-requests',
14511
14830
  autoShow: true,
14512
14831
  debug: false,
14513
14832
  mock: false,
@@ -14558,7 +14877,12 @@
14558
14877
 
14559
14878
  _bindMethods() {
14560
14879
  this.createWidget = this.createWidget.bind(this);
14880
+ this.createFeedbackWidget = this.createFeedbackWidget.bind(this);
14881
+ this.createMessengerWidget = this.createMessengerWidget.bind(this);
14882
+ this.createChangelogWidget = this.createChangelogWidget.bind(this);
14883
+ this.createSurveyWidget = this.createSurveyWidget.bind(this);
14561
14884
  this.destroyWidget = this.destroyWidget.bind(this);
14885
+ this.identify = this.identify.bind(this);
14562
14886
  this.updateConfig = this.updateConfig.bind(this);
14563
14887
  }
14564
14888
 
@@ -14594,6 +14918,49 @@
14594
14918
  : undefined,
14595
14919
  };
14596
14920
  }
14921
+
14922
+ async _syncConfiguredMetadataAfterInit() {
14923
+ if (
14924
+ !this.config.metadata ||
14925
+ this.apiService.identitySyncedToken === this.apiService.sessionToken
14926
+ ) {
14927
+ return null;
14928
+ }
14929
+
14930
+ try {
14931
+ return await this.identify(this.config.metadata);
14932
+ } catch (error) {
14933
+ if (this.config.debug) {
14934
+ console.warn('[Product7] Initial identify failed:', error);
14935
+ }
14936
+ return null;
14937
+ }
14938
+ }
14939
+
14940
+ _extractIdentifyConfig(response) {
14941
+ const payload = this._isPlainObject(response?.data)
14942
+ ? response.data
14943
+ : response || {};
14944
+ const configPatch = this._isPlainObject(payload.config)
14945
+ ? payload.config
14946
+ : {};
14947
+
14948
+ if (payload.last_feedback_at !== undefined) {
14949
+ configPatch.last_feedback_at = payload.last_feedback_at;
14950
+ } else if (payload.lastFeedbackAt !== undefined) {
14951
+ configPatch.last_feedback_at = payload.lastFeedbackAt;
14952
+ }
14953
+
14954
+ return this._omitUndefined(configPatch);
14955
+ }
14956
+
14957
+ _applyIdentityToWidgets(metadata) {
14958
+ for (const widget of this.widgets.values()) {
14959
+ if (typeof widget.applyIdentity === 'function') {
14960
+ widget.applyIdentity(metadata);
14961
+ }
14962
+ }
14963
+ }
14597
14964
  };
14598
14965
 
14599
14966
  // --- Identify: transform flat user data into internal format ---
@@ -14676,9 +15043,21 @@
14676
15043
  return obj;
14677
15044
  }
14678
15045
 
15046
+ function hasIdentifyMetadata(metadata = {}) {
15047
+ return Object.values(metadata).some((value) => value !== undefined);
15048
+ }
15049
+
14679
15050
  // --- Ensure SDK is initialized (shared by widget inits) ---
14680
15051
 
14681
15052
  async function ensureSDK(options) {
15053
+ if (
15054
+ Product7._sdk &&
15055
+ options.organization &&
15056
+ Product7._sdk.config.workspace !== options.organization
15057
+ ) {
15058
+ Product7.destroy();
15059
+ }
15060
+
14682
15061
  if (Product7._sdk) return Product7._sdk;
14683
15062
 
14684
15063
  if (options.organization) {
@@ -14721,63 +15100,58 @@
14721
15100
  async identify(data = {}, callback) {
14722
15101
  try {
14723
15102
  const transformed = transformIdentifyData(data);
14724
- Product7._organization = transformed.workspace;
15103
+ Product7._organization =
15104
+ transformed.workspace || Product7._organization || null;
14725
15105
 
14726
15106
  const config = cleanUndefined({
14727
- workspace: transformed.workspace,
14728
- metadata: transformed.metadata,
15107
+ workspace: Product7._organization,
14729
15108
  debug: transformed.debug,
14730
15109
  mock: transformed.mock,
14731
15110
  env: transformed.env,
14732
15111
  apiUrl: transformed.apiUrl,
14733
15112
  });
14734
15113
 
14735
- const sdk = new Product7$1(config);
14736
- const initData = await sdk.init();
14737
-
14738
- Product7._sdk = sdk;
14739
- Product7._identified = true;
15114
+ let sdk = Product7._sdk;
15115
+ const requiresNewSDK =
15116
+ !sdk || (config.workspace && sdk.config.workspace !== config.workspace);
14740
15117
 
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
- }
15118
+ if (requiresNewSDK) {
15119
+ if (Product7._sdk) {
15120
+ Product7.destroy();
15121
+ Product7._organization = config.workspace || null;
14766
15122
  }
15123
+ sdk = new Product7$1(config);
15124
+ Product7._sdk = sdk;
14767
15125
  }
14768
15126
 
15127
+ const initData = sdk.initialized
15128
+ ? {
15129
+ alreadyInitialized: true,
15130
+ sessionToken: sdk.apiService?.sessionToken,
15131
+ }
15132
+ : await sdk.init();
15133
+
15134
+ const identifyData = hasIdentifyMetadata(transformed.metadata)
15135
+ ? await sdk.identify(transformed.metadata)
15136
+ : null;
15137
+ Product7._identified = Boolean(identifyData?.identified);
15138
+
14769
15139
  Product7._flushQueue();
14770
15140
 
14771
15141
  if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
14772
15142
  window.dispatchEvent(
14773
15143
  new CustomEvent('Product7Ready', {
14774
- detail: { sdk, config, initData },
15144
+ detail: { sdk, config: sdk.config, initData, identifyData },
14775
15145
  })
14776
15146
  );
14777
15147
  }
14778
15148
 
14779
15149
  if (typeof callback === 'function') callback(null);
14780
- return initData;
15150
+ return {
15151
+ ...initData,
15152
+ identified: Product7._identified,
15153
+ identify: identifyData,
15154
+ };
14781
15155
  } catch (error) {
14782
15156
  console.error('[Product7] Identify failed:', error);
14783
15157
 
@@ -14836,7 +15210,7 @@
14836
15210
  if (!options.placement) {
14837
15211
  widgetOptions.autoShow = false;
14838
15212
  widgetOptions.displayMode = 'modal';
14839
- widgetOptions._noTriggerButton = true;
15213
+ widgetOptions.headless = true;
14840
15214
  } else {
14841
15215
  // Trigger button is always visible when placement is set
14842
15216
  widgetOptions.suppressAfterSubmission = false;
@@ -14844,7 +15218,7 @@
14844
15218
  }
14845
15219
 
14846
15220
  try {
14847
- const widget = sdk.createWidget('button', widgetOptions);
15221
+ const widget = sdk.createFeedbackWidget(widgetOptions);
14848
15222
  widget.mount();
14849
15223
 
14850
15224
  if (options.placement) {
@@ -14883,13 +15257,13 @@
14883
15257
  if (options.setBoard) {
14884
15258
  Product7._feedbackWidget.options.boardName = options.setBoard;
14885
15259
  }
14886
- Product7._feedbackWidget.openPanel();
15260
+ Product7._feedbackWidget.open();
14887
15261
  }
14888
15262
  },
14889
15263
 
14890
15264
  closeFeedback() {
14891
15265
  if (Product7._feedbackWidget) {
14892
- Product7._feedbackWidget.closePanel();
15266
+ Product7._feedbackWidget.close();
14893
15267
  }
14894
15268
  },
14895
15269
 
@@ -14948,7 +15322,7 @@
14948
15322
  widgetOptions.enabled = true;
14949
15323
 
14950
15324
  try {
14951
- const widget = sdk.createWidget('messenger', widgetOptions);
15325
+ const widget = sdk.createMessengerWidget(widgetOptions);
14952
15326
  widget.mount();
14953
15327
  widget.show();
14954
15328