@product7/product7-js 0.3.1 → 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;
@@ -11243,6 +11370,7 @@
11243
11370
  class MessengerWidget extends BaseWidget {
11244
11371
  constructor(options) {
11245
11372
  super({ ...options, type: 'messenger' });
11373
+ this._explicitOptions = options || {};
11246
11374
  const resolvedTheme = options.theme || 'light';
11247
11375
  const hasExplicitTextColor = Object.prototype.hasOwnProperty.call(
11248
11376
  options,
@@ -11325,6 +11453,10 @@
11325
11453
  this._handleConversationClosed = this._handleConversationClosed.bind(this);
11326
11454
  }
11327
11455
 
11456
+ _hasTrigger() {
11457
+ return this.options.trigger === true || this.options.trigger === undefined;
11458
+ }
11459
+
11328
11460
  _createInternalFeedbackWidget() {
11329
11461
  try {
11330
11462
  const widget = this.sdk.createWidget('button', {
@@ -11360,11 +11492,13 @@
11360
11492
  this._feedbackWidget = this._createInternalFeedbackWidget();
11361
11493
  }
11362
11494
 
11363
- this.launcher = new MessengerLauncher(this.messengerState, {
11364
- position: this.messengerOptions.position,
11365
- primaryColor: this.messengerOptions.primaryColor,
11366
- });
11367
- 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
+ }
11368
11502
 
11369
11503
  this.panel = new MessengerPanel(this.messengerState, {
11370
11504
  position: this.messengerOptions.position,
@@ -11499,23 +11633,13 @@
11499
11633
 
11500
11634
  async _handleIdentifyContact(contactData) {
11501
11635
  try {
11502
- const response = await this.apiService.identifyContact({
11503
- 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({
11504
11639
  email: contactData.email,
11640
+ name: contactData.name,
11505
11641
  });
11506
-
11507
- if (response.status) {
11508
- console.log(
11509
- '[MessengerWidget] Contact identified:',
11510
- response.data.contact_id
11511
- );
11512
- this.messengerState.setIdentified(true, {
11513
- name: contactData.name,
11514
- email: contactData.email,
11515
- });
11516
- }
11517
-
11518
- return response;
11642
+ return result;
11519
11643
  } catch (error) {
11520
11644
  console.error('[MessengerWidget] Failed to identify contact:', error);
11521
11645
  throw error;
@@ -11523,8 +11647,17 @@
11523
11647
  }
11524
11648
 
11525
11649
  markAsIdentified(name, email) {
11526
- this.messengerState.setIdentified(true, { name, email });
11527
- 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);
11657
+
11658
+ if (this.apiService?.sessionToken && !this.wsService?.isConnected) {
11659
+ this._initWebSocket();
11660
+ }
11528
11661
  }
11529
11662
 
11530
11663
  async _handleUploadFile(base64Data, filename) {
@@ -12035,6 +12168,42 @@
12035
12168
  }
12036
12169
  }
12037
12170
 
12171
+ async _fetchAndApplySettings() {
12172
+ try {
12173
+ const response = await this.apiService.getMessengerSettings();
12174
+ if (!response?.status || !response?.data) return;
12175
+
12176
+ const s = response.data;
12177
+
12178
+ // Only apply values that were NOT explicitly passed in options
12179
+ if (s.team_name && !this._hasExplicitOption('teamName')) {
12180
+ this.messengerOptions.teamName = s.team_name;
12181
+ this.messengerState.teamName = s.team_name;
12182
+ }
12183
+ if (s.logo_url && !this._hasExplicitOption('logoUrl')) {
12184
+ this.messengerOptions.logoUrl = s.logo_url;
12185
+ }
12186
+ if (s.greeting_message && !this._hasExplicitOption('greetingMessage')) {
12187
+ this.messengerState.greetingMessage = s.greeting_message;
12188
+ }
12189
+ if (s.response_time && !this._hasExplicitOption('responseTime')) {
12190
+ this.messengerState.responseTime = s.response_time;
12191
+ }
12192
+
12193
+ // Notify views to re-render with new values
12194
+ this.messengerState._notify('availabilityUpdate', {});
12195
+ } catch (e) {
12196
+ // non-fatal
12197
+ }
12198
+ }
12199
+
12200
+ _hasExplicitOption(key) {
12201
+ return Object.prototype.hasOwnProperty.call(
12202
+ this._explicitOptions || {},
12203
+ key
12204
+ );
12205
+ }
12206
+
12038
12207
  async checkAgentAvailability() {
12039
12208
  try {
12040
12209
  const response = await this.apiService.checkAgentsOnline();
@@ -12180,6 +12349,9 @@
12180
12349
  this._applyPreviewData();
12181
12350
 
12182
12351
  if (this.messengerOptions.autoLoadData) {
12352
+ // Fetch workspace settings and apply only if not explicitly configured
12353
+ this._fetchAndApplySettings();
12354
+
12183
12355
  this.loadInitialData();
12184
12356
 
12185
12357
  if (this.apiService?.sessionToken) {
@@ -12324,6 +12496,7 @@
12324
12496
 
12325
12497
  SurveyWidget.removeDanglingElements();
12326
12498
  this._renderSurvey();
12499
+ this.state.isOpen = true;
12327
12500
  this.surveyState.isVisible = true;
12328
12501
  this.sdk.eventBus.emit('survey:shown', {
12329
12502
  widget: this,
@@ -12333,6 +12506,7 @@
12333
12506
  }
12334
12507
 
12335
12508
  hide() {
12509
+ this.state.isOpen = false;
12336
12510
  this._closeSurvey();
12337
12511
  return this;
12338
12512
  }
@@ -12407,6 +12581,7 @@
12407
12581
  this._attachSurveyEvents();
12408
12582
 
12409
12583
  requestAnimationFrame(() => {
12584
+ if (!this.surveyElement) return;
12410
12585
  this.surveyElement.style.opacity = '1';
12411
12586
  this.surveyElement.style.transform =
12412
12587
  this.surveyOptions.position === 'center'
@@ -13544,6 +13719,7 @@
13544
13719
  }
13545
13720
 
13546
13721
  _closeSurvey(resetState = true, immediate = false) {
13722
+ this.state.isOpen = false;
13547
13723
  if (this._escapeHandler) {
13548
13724
  document.removeEventListener('keydown', this._escapeHandler);
13549
13725
  this._escapeHandler = null;
@@ -13606,6 +13782,22 @@
13606
13782
  this.sdk.eventBus.emit('survey:closed', { widget: this });
13607
13783
  }
13608
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
+
13609
13801
  destroy() {
13610
13802
  this._closeSurvey(true, true);
13611
13803
  super.destroy();
@@ -13723,6 +13915,7 @@
13723
13915
  constructor(config = {}) {
13724
13916
  this.config = this._validateAndMergeConfig(config);
13725
13917
  this.initialized = false;
13918
+ this.identified = false;
13726
13919
  this.widgets = new Map();
13727
13920
  this.eventBus = new EventBus();
13728
13921
 
@@ -13767,13 +13960,14 @@
13767
13960
  this._injectStyles();
13768
13961
 
13769
13962
  try {
13770
- const initData = await this.apiService.init(this.config.metadata);
13963
+ const initData = await this.apiService.init();
13771
13964
 
13772
13965
  if (initData.config) {
13773
13966
  this.config = deepMerge(initData.config, this.config);
13774
13967
  }
13775
13968
 
13776
13969
  this.initialized = true;
13970
+ const identifyResult = await this._syncConfiguredMetadataAfterInit();
13777
13971
  this.eventBus.emit('sdk:initialized', {
13778
13972
  config: this.config,
13779
13973
  sessionToken: initData.sessionToken,
@@ -13784,6 +13978,7 @@
13784
13978
  config: initData.config || {},
13785
13979
  sessionToken: initData.sessionToken,
13786
13980
  expiresIn: initData.expiresIn,
13981
+ identified: Boolean(identifyResult?.identified),
13787
13982
  };
13788
13983
  } catch (error) {
13789
13984
  this.eventBus.emit('sdk:error', { error });
@@ -13798,10 +13993,14 @@
13798
13993
  );
13799
13994
  }
13800
13995
 
13996
+ const requestedType = type || 'button';
13997
+ const normalizedType = this._normalizeWidgetType(requestedType);
13801
13998
  const widgetId = generateId('widget');
13802
- const widgetConfig = this._getWidgetTypeConfig(type);
13803
- const explicitOptions = this._omitUndefined(options);
13804
- 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, {
13805
14004
  ...widgetConfig,
13806
14005
  ...explicitOptions,
13807
14006
  });
@@ -13816,15 +14015,35 @@
13816
14015
  };
13817
14016
 
13818
14017
  try {
13819
- const widget = WidgetFactory.create(type, widgetOptions);
14018
+ const widget = WidgetFactory.create(normalizedType, widgetOptions);
13820
14019
  this.widgets.set(widgetId, widget);
13821
- this.eventBus.emit('widget:created', { widget, type });
14020
+ this.eventBus.emit('widget:created', {
14021
+ widget,
14022
+ type: requestedType,
14023
+ internalType: normalizedType,
14024
+ });
13822
14025
  return widget;
13823
14026
  } catch (error) {
13824
14027
  throw new SDKError(`Failed to create widget: ${error.message}`, error);
13825
14028
  }
13826
14029
  }
13827
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
+
13828
14047
  getWidget(id) {
13829
14048
  return this.widgets.get(id);
13830
14049
  }
@@ -13943,7 +14162,7 @@
13943
14162
  return null;
13944
14163
  }
13945
14164
 
13946
- const surveyWidget = this.createWidget('survey', {
14165
+ const surveyWidget = this.createSurveyWidget({
13947
14166
  surveyId: normalizedOptions.surveyId,
13948
14167
  surveyType:
13949
14168
  normalizedOptions.surveyType || normalizedOptions.type || 'nps',
@@ -14177,18 +14396,34 @@
14177
14396
  ? this.config.widgets
14178
14397
  : {};
14179
14398
 
14180
- const legacyTypeConfig = this._isPlainObject(this.config?.[type])
14181
- ? this.config[type]
14182
- : {};
14399
+ const mergedTypeConfig = this._getWidgetTypeAliases(type).reduce(
14400
+ (config, alias) => {
14401
+ const legacyTypeConfig = this._isPlainObject(this.config?.[alias])
14402
+ ? this.config[alias]
14403
+ : {};
14183
14404
 
14184
- const namespacedTypeConfig = this._isPlainObject(widgetsConfig?.[type])
14185
- ? widgetsConfig[type]
14186
- : {};
14405
+ const namespacedTypeConfig = this._isPlainObject(widgetsConfig?.[alias])
14406
+ ? widgetsConfig[alias]
14407
+ : {};
14187
14408
 
14188
- const mergedTypeConfig = deepMerge(legacyTypeConfig, namespacedTypeConfig);
14409
+ return deepMerge(
14410
+ config,
14411
+ deepMerge(legacyTypeConfig, namespacedTypeConfig)
14412
+ );
14413
+ },
14414
+ {}
14415
+ );
14189
14416
  return this._toCamelCaseObject(mergedTypeConfig);
14190
14417
  }
14191
14418
 
14419
+ _getWidgetTypeAliases(type) {
14420
+ if (type === 'button') {
14421
+ return ['button', 'feedback'];
14422
+ }
14423
+
14424
+ return [type];
14425
+ }
14426
+
14192
14427
  _isWidgetEnabled(type, options = {}) {
14193
14428
  const typeConfig = this._getWidgetTypeConfig(type);
14194
14429
  if (typeConfig.enabled === false) {
@@ -14231,6 +14466,31 @@
14231
14466
  return normalized;
14232
14467
  }
14233
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
+
14234
14494
  _omitUndefined(value) {
14235
14495
  if (!this._isPlainObject(value)) {
14236
14496
  return value;
@@ -14275,7 +14535,7 @@
14275
14535
  return null;
14276
14536
  }
14277
14537
 
14278
- const changelogWidget = this.createWidget('changelog', {
14538
+ const changelogWidget = this.createChangelogWidget({
14279
14539
  ...defaults,
14280
14540
  ...configDefaults,
14281
14541
  ...explicitOptions,
@@ -14342,10 +14602,15 @@
14342
14602
  }
14343
14603
 
14344
14604
  setMetadata(metadata) {
14605
+ if (metadata) {
14606
+ this._validateMetadata(metadata);
14607
+ }
14608
+
14345
14609
  this.config.metadata = metadata;
14346
14610
  if (this.apiService) {
14347
14611
  this.apiService.setMetadata(metadata);
14348
14612
  }
14613
+ this.identified = false;
14349
14614
  this.eventBus.emit('metadata:updated', { metadata });
14350
14615
  }
14351
14616
 
@@ -14356,11 +14621,53 @@
14356
14621
  );
14357
14622
  }
14358
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
+
14359
14665
  async reinitialize(newMetadata = null) {
14360
14666
  this.apiService.clearSession();
14361
14667
  this.initialized = false;
14668
+ this.identified = false;
14362
14669
 
14363
- if (newMetadata) {
14670
+ if (newMetadata !== null) {
14364
14671
  this.setMetadata(newMetadata);
14365
14672
  }
14366
14673
 
@@ -14389,10 +14696,11 @@
14389
14696
 
14390
14697
  destroy() {
14391
14698
  this.destroyAllWidgets();
14392
- this.eventBus.removeAllListeners();
14699
+ this.eventBus.emit('sdk:destroyed');
14700
+ this.eventBus.clear();
14393
14701
  this.apiService.clearSession();
14394
14702
  this.initialized = false;
14395
- this.eventBus.emit('sdk:destroyed');
14703
+ this.identified = false;
14396
14704
  }
14397
14705
 
14398
14706
  hasFeedbackBeenSubmitted(cooldownDays = 30) {
@@ -14511,7 +14819,12 @@
14511
14819
 
14512
14820
  _bindMethods() {
14513
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);
14514
14826
  this.destroyWidget = this.destroyWidget.bind(this);
14827
+ this.identify = this.identify.bind(this);
14515
14828
  this.updateConfig = this.updateConfig.bind(this);
14516
14829
  }
14517
14830
 
@@ -14547,6 +14860,49 @@
14547
14860
  : undefined,
14548
14861
  };
14549
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
+ }
14550
14906
  };
14551
14907
 
14552
14908
  // --- Identify: transform flat user data into internal format ---
@@ -14629,9 +14985,21 @@
14629
14985
  return obj;
14630
14986
  }
14631
14987
 
14988
+ function hasIdentifyMetadata(metadata = {}) {
14989
+ return Object.values(metadata).some((value) => value !== undefined);
14990
+ }
14991
+
14632
14992
  // --- Ensure SDK is initialized (shared by widget inits) ---
14633
14993
 
14634
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
+
14635
15003
  if (Product7._sdk) return Product7._sdk;
14636
15004
 
14637
15005
  if (options.organization) {
@@ -14674,63 +15042,58 @@
14674
15042
  async identify(data = {}, callback) {
14675
15043
  try {
14676
15044
  const transformed = transformIdentifyData(data);
14677
- Product7._organization = transformed.workspace;
15045
+ Product7._organization =
15046
+ transformed.workspace || Product7._organization || null;
14678
15047
 
14679
15048
  const config = cleanUndefined({
14680
- workspace: transformed.workspace,
14681
- metadata: transformed.metadata,
15049
+ workspace: Product7._organization,
14682
15050
  debug: transformed.debug,
14683
15051
  mock: transformed.mock,
14684
15052
  env: transformed.env,
14685
15053
  apiUrl: transformed.apiUrl,
14686
15054
  });
14687
15055
 
14688
- const sdk = new Product7$1(config);
14689
- const initData = await sdk.init();
14690
-
14691
- Product7._sdk = sdk;
14692
- Product7._identified = true;
15056
+ let sdk = Product7._sdk;
15057
+ const requiresNewSDK =
15058
+ !sdk || (config.workspace && sdk.config.workspace !== config.workspace);
14693
15059
 
14694
- // Sync custom attributes + segments via /widget/identify
14695
- if (transformed.metadata && sdk.apiService?.sessionToken) {
14696
- try {
14697
- const identifyPayload = {
14698
- user_id: transformed.metadata.user_id,
14699
- email: transformed.metadata.email,
14700
- name: transformed.metadata.name,
14701
- avatar: transformed.metadata.profile_picture,
14702
- attributes: transformed.metadata.custom_fields || {},
14703
- };
14704
- if (transformed.metadata.company) {
14705
- identifyPayload.company = transformed.metadata.company;
14706
- }
14707
- await sdk.apiService._makeRequest('/widget/identify', {
14708
- method: 'POST',
14709
- body: JSON.stringify(identifyPayload),
14710
- headers: {
14711
- 'Content-Type': 'application/json',
14712
- Authorization: `Bearer ${sdk.apiService.sessionToken}`,
14713
- },
14714
- });
14715
- } catch (identifyErr) {
14716
- if (config.debug) {
14717
- console.warn('[Product7] Attribute sync failed:', identifyErr);
14718
- }
15060
+ if (requiresNewSDK) {
15061
+ if (Product7._sdk) {
15062
+ Product7.destroy();
15063
+ Product7._organization = config.workspace || null;
14719
15064
  }
15065
+ sdk = new Product7$1(config);
15066
+ Product7._sdk = sdk;
14720
15067
  }
14721
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
+
14722
15081
  Product7._flushQueue();
14723
15082
 
14724
15083
  if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
14725
15084
  window.dispatchEvent(
14726
15085
  new CustomEvent('Product7Ready', {
14727
- detail: { sdk, config, initData },
15086
+ detail: { sdk, config: sdk.config, initData, identifyData },
14728
15087
  })
14729
15088
  );
14730
15089
  }
14731
15090
 
14732
15091
  if (typeof callback === 'function') callback(null);
14733
- return initData;
15092
+ return {
15093
+ ...initData,
15094
+ identified: Product7._identified,
15095
+ identify: identifyData,
15096
+ };
14734
15097
  } catch (error) {
14735
15098
  console.error('[Product7] Identify failed:', error);
14736
15099
 
@@ -14789,7 +15152,7 @@
14789
15152
  if (!options.placement) {
14790
15153
  widgetOptions.autoShow = false;
14791
15154
  widgetOptions.displayMode = 'modal';
14792
- widgetOptions._noTriggerButton = true;
15155
+ widgetOptions.headless = true;
14793
15156
  } else {
14794
15157
  // Trigger button is always visible when placement is set
14795
15158
  widgetOptions.suppressAfterSubmission = false;
@@ -14797,7 +15160,7 @@
14797
15160
  }
14798
15161
 
14799
15162
  try {
14800
- const widget = sdk.createWidget('button', widgetOptions);
15163
+ const widget = sdk.createFeedbackWidget(widgetOptions);
14801
15164
  widget.mount();
14802
15165
 
14803
15166
  if (options.placement) {
@@ -14836,13 +15199,13 @@
14836
15199
  if (options.setBoard) {
14837
15200
  Product7._feedbackWidget.options.boardName = options.setBoard;
14838
15201
  }
14839
- Product7._feedbackWidget.openPanel();
15202
+ Product7._feedbackWidget.open();
14840
15203
  }
14841
15204
  },
14842
15205
 
14843
15206
  closeFeedback() {
14844
15207
  if (Product7._feedbackWidget) {
14845
- Product7._feedbackWidget.closePanel();
15208
+ Product7._feedbackWidget.close();
14846
15209
  }
14847
15210
  },
14848
15211
 
@@ -14901,7 +15264,7 @@
14901
15264
  widgetOptions.enabled = true;
14902
15265
 
14903
15266
  try {
14904
- const widget = sdk.createWidget('messenger', widgetOptions);
15267
+ const widget = sdk.createMessengerWidget(widgetOptions);
14905
15268
  widget.mount();
14906
15269
  widget.show();
14907
15270