@meetelise/chat 1.20.90 → 1.20.92

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.
package/src/MyPubnub.ts CHANGED
@@ -9,6 +9,7 @@ import addHours from "date-fns/addHours";
9
9
  import isAfter from "date-fns/isAfter";
10
10
  import formatISO from "date-fns/formatISO";
11
11
  import isBefore from "date-fns/isBefore";
12
+ import { LogType, sendLoggingEvent } from "./analytics";
12
13
 
13
14
  interface TokenResponse {
14
15
  auth: {
@@ -44,8 +45,11 @@ class MyPubnub {
44
45
 
45
46
  private building: Building | null = null;
46
47
  private buildingSlug: string;
48
+ private orgSlug: string;
47
49
  private ttlHours = 24;
48
50
 
51
+ private eliseResponseTimeout: NodeJS.Timeout | null = null;
52
+
49
53
  pubnub: Pubnub | null = null;
50
54
  leadUserId = "";
51
55
  channel = "";
@@ -72,14 +76,22 @@ class MyPubnub {
72
76
  messages: this.messages,
73
77
  isLoading: isWaitingForEliseResponse,
74
78
  });
79
+ if (!isWaitingForEliseResponse && this.eliseResponseTimeout) {
80
+ clearTimeout(this.eliseResponseTimeout);
81
+ }
75
82
  this.isLoadingMessages = isWaitingForEliseResponse;
76
83
  },
77
84
  };
78
85
  isLoadingMessages = false;
79
86
 
80
- constructor(buildingSlug: string, buildingDetails: Building) {
87
+ constructor(
88
+ buildingSlug: string,
89
+ buildingDetails: Building,
90
+ orgSlug: string
91
+ ) {
81
92
  this.buildingSlug = buildingSlug;
82
93
  this.building = buildingDetails;
94
+ this.orgSlug = orgSlug;
83
95
  }
84
96
 
85
97
  addChatListener(
@@ -95,7 +107,17 @@ class MyPubnub {
95
107
  const storedChatKeyValues = this.getChatStorageKey();
96
108
  if (!storedChatKeyValues.leadId) {
97
109
  // eslint-disable-next-line no-console
98
- console.error("Error getting chat storage key...");
110
+ sendLoggingEvent({
111
+ logTitle: "PUBNUB_ERROR_FETCHING_STORAGE_KEY",
112
+ logData: {
113
+ channel: this.channel,
114
+ eliseaiLocalStorageValue: storedChatKeyValues,
115
+ leadUserId: this.leadUserId,
116
+ },
117
+ logType: LogType.warn,
118
+ buildingSlug: this.buildingSlug,
119
+ orgSlug: this.orgSlug,
120
+ });
99
121
  return;
100
122
  }
101
123
 
@@ -115,6 +137,7 @@ class MyPubnub {
115
137
  }
116
138
 
117
139
  const pubnubToken = await this.fetchToken(this.leadUserId, this.channel);
140
+ if (!pubnubToken) return;
118
141
 
119
142
  // These keys are OK to expose live, the authKey generated by the BE is what
120
143
  // is used to authenticate the user. Ideally, should also add rate limiting
@@ -129,17 +152,42 @@ class MyPubnub {
129
152
  await this.withAuthToken(() => this.getChannelHistory());
130
153
  return this.pubnub;
131
154
  }
132
- async fetchToken(lead: string, channel: string): Promise<TokenResponse> {
133
- const response = await axios.get(
134
- `${this.apiHost}/platformApi/webchat/pn/request-token?user_id=${lead}&channel=${channel}`
135
- );
136
- return response.data;
155
+ async fetchToken(
156
+ lead: string,
157
+ channel: string
158
+ ): Promise<TokenResponse | null> {
159
+ try {
160
+ const response = await axios.get(
161
+ `${this.apiHost}/platformApi/webchat/pn/request-token?user_id=${lead}&channel=${channel}`
162
+ );
163
+ return response.data;
164
+ } catch (error) {
165
+ sendLoggingEvent({
166
+ logTitle: "PUBNUB_ERROR_FETCHING_TOKEN",
167
+ logData: { error },
168
+ logType: LogType.error,
169
+ buildingSlug: this.buildingSlug,
170
+ orgSlug: this.orgSlug,
171
+ });
172
+ }
173
+ return null;
137
174
  }
138
175
  async fetchChannelExists(channel: string): Promise<boolean> {
139
- const response = await axios.get(
140
- `${this.apiHost}/platformApi/webchat/check-channel-exists?channel_name=${channel}`
141
- );
142
- return response.data;
176
+ try {
177
+ const response = await axios.get(
178
+ `${this.apiHost}/platformApi/webchat/check-channel-exists?channel_name=${channel}`
179
+ );
180
+ return response.data;
181
+ } catch (error) {
182
+ sendLoggingEvent({
183
+ logTitle: "PUBNUB_ERROR_FETCHING_CHANNEL_EXISTS",
184
+ logData: { error },
185
+ logType: LogType.error,
186
+ buildingSlug: this.buildingSlug,
187
+ orgSlug: this.orgSlug,
188
+ });
189
+ }
190
+ return false;
143
191
  }
144
192
 
145
193
  async withAuthToken(apiRequestFunc: () => Promise<void>): Promise<void> {
@@ -163,7 +211,15 @@ class MyPubnub {
163
211
 
164
212
  await apiRequestFunc();
165
213
  } catch (retryError) {
166
- //onsole.error(retryError);
214
+ sendLoggingEvent({
215
+ logTitle: "PUBNUB_ERROR_REFETCHING_TOKEN",
216
+ logData: {
217
+ retryError,
218
+ },
219
+ logType: LogType.error,
220
+ buildingSlug: this.buildingSlug,
221
+ orgSlug: this.orgSlug,
222
+ });
167
223
  }
168
224
  }
169
225
  }
@@ -181,6 +237,17 @@ class MyPubnub {
181
237
  },
182
238
  (status, response) => {
183
239
  if (status.error) {
240
+ sendLoggingEvent({
241
+ logTitle: "PUBNUB_ERROR_FETCHING_HISTORY",
242
+ logData: {
243
+ channel: this.channel,
244
+ status: status,
245
+ response: response,
246
+ },
247
+ logType: LogType.error,
248
+ buildingSlug: this.buildingSlug,
249
+ orgSlug: this.orgSlug,
250
+ });
184
251
  reject(status);
185
252
  } else {
186
253
  resolve(response);
@@ -208,14 +275,35 @@ class MyPubnub {
208
275
  this.chatListener?.({ messages: this.messages, isLoading: false });
209
276
  }
210
277
  } catch (error) {
211
- // Handle the error here
278
+ sendLoggingEvent({
279
+ logTitle: "PUBNUB_ERROR_FETCHING_HISTORY",
280
+ logData: { error },
281
+ logType: LogType.error,
282
+ buildingSlug: this.buildingSlug,
283
+ orgSlug: this.orgSlug,
284
+ });
212
285
  }
213
286
  }
214
287
 
215
288
  handleChatListeners = (): void => {
216
289
  if (!this.pubnub || !this.channel) return;
217
- this.pubnub.subscribe({ channels: [this.channel] });
218
- this.pubnub.addListener(this.listenerParams);
290
+ try {
291
+ this.pubnub.subscribe({ channels: [this.channel] });
292
+ this.pubnub.addListener(this.listenerParams);
293
+ } catch (error) {
294
+ sendLoggingEvent({
295
+ logTitle: "PUBNUB_ERROR_ADDING_LISTENER",
296
+ logData: {
297
+ error,
298
+ channel: this.channel,
299
+ leadUserId: this.leadUserId,
300
+ website: location.href,
301
+ },
302
+ logType: LogType.error,
303
+ buildingSlug: this.buildingSlug,
304
+ orgSlug: this.orgSlug,
305
+ });
306
+ }
219
307
  };
220
308
  removeChatListeners = (): void => {
221
309
  if (this.pubnub && this.channel) {
@@ -234,6 +322,23 @@ class MyPubnub {
234
322
 
235
323
  await this.withAuthToken(async () => {
236
324
  if (!this.pubnub || !this.channel) return;
325
+
326
+ this.eliseResponseTimeout = setTimeout(() => {
327
+ // eslint-disable-next-line no-console
328
+ console.error("Elise AI did not respond in time...");
329
+ sendLoggingEvent({
330
+ logTitle: "PUBNUB_ERROR_ELISEAI_MESSAGE_TIMEOUT",
331
+ logData: {
332
+ channel: this.channel,
333
+ leadUserId: this.leadUserId,
334
+ message,
335
+ },
336
+ logType: LogType.error,
337
+ buildingSlug: this.buildingSlug,
338
+ orgSlug: this.orgSlug,
339
+ });
340
+ }, 30000); // if after 30 seconds, no message - we log error
341
+
237
342
  await this.pubnub.publish({
238
343
  channel: this.channel,
239
344
  message: {
@@ -269,7 +374,7 @@ class MyPubnub {
269
374
  buildingSlug: this.buildingSlug,
270
375
  };
271
376
  };
272
- getChatStorageKey = (): ChatInfo => {
377
+ getChatStorageKey = (createNewIfNotExist = true): ChatInfo => {
273
378
  const eliseaiLocalStorageValue = localStorage.getItem(
274
379
  "com.eliseai.webchat.slug=" + this.buildingSlug
275
380
  );
@@ -300,7 +405,12 @@ class MyPubnub {
300
405
  }
301
406
  }
302
407
 
303
- return this.createChatStorageKey();
408
+ if (createNewIfNotExist) return this.createChatStorageKey();
409
+ return {
410
+ leadId: null,
411
+ timestamp: null,
412
+ buildingSlug: null,
413
+ };
304
414
  };
305
415
  isChatKeyValid = (storageValueDeconstructed: ChatInfo): boolean => {
306
416
  if (
@@ -484,9 +484,20 @@ export class MEChat extends LitElement {
484
484
  initializePubnubVariables = async (): Promise<void> => {
485
485
  await this.setBuildingDerivedInfo();
486
486
  if (!this.building) return;
487
+ if (this.building.conversationMaintenanceMode) {
488
+ // eslint-disable-next-line no-console
489
+ console.warn(
490
+ "MeetElise Chat is in maintenance mode. Chat icon will not appear."
491
+ );
492
+ return;
493
+ }
487
494
 
488
- this.myPubnub = new MyPubnub(this.buildingSlug, this.building);
489
- if (this.myPubnub.isChatKeyValid(this.myPubnub.getChatStorageKey())) {
495
+ this.myPubnub = new MyPubnub(
496
+ this.buildingSlug,
497
+ this.building,
498
+ this.orgSlug
499
+ );
500
+ if (this.myPubnub.isChatKeyValid(this.myPubnub.getChatStorageKey(false))) {
490
501
  await this.myPubnub.initializePubnub();
491
502
  }
492
503
  this.attachOnClickToLauncher();
@@ -4,7 +4,7 @@ export const pubnubChatStyles = css`
4
4
  #pubnub-chat-container {
5
5
  position: fixed;
6
6
 
7
- z-index: 100000;
7
+ z-index: 100001;
8
8
  display: flex;
9
9
  align-items: center;
10
10
 
@@ -127,7 +127,7 @@ export const pubnubChatStyles = css`
127
127
  }
128
128
 
129
129
  #loading-message {
130
- padding: 16px;
130
+ padding: 12px;
131
131
  }
132
132
  .loading-dot {
133
133
  width: 6px;
@@ -178,6 +178,7 @@ export const pubnubChatStyles = css`
178
178
  box-sizing: border-box;
179
179
  gap: 16px;
180
180
  padding: 24px;
181
+ z-index: 100001;
181
182
  }
182
183
  #message-input {
183
184
  height: 40px;
@@ -189,6 +190,7 @@ export const pubnubChatStyles = css`
189
190
  border: none;
190
191
  color: white;
191
192
  background: none;
193
+ z-index: 1000000000000000000000000001;
192
194
  }
193
195
  #message-input:focus {
194
196
  outline: none;
@@ -74,11 +74,6 @@ export class PubnubChat extends LitElement {
74
74
  }
75
75
  );
76
76
  this.onMount();
77
- // TODO (erol): scroll to bottom on all children load
78
- // this is a hacky way to do it
79
- setTimeout(() => {
80
- this.scrollToChatBottom();
81
- }, 1000);
82
77
  }
83
78
 
84
79
  scrollToChatBottom = (): void => {
package/src/analytics.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import axios from "axios";
2
+
1
3
  declare global {
2
4
  interface Window {
3
5
  RCTPCampaign?: { CampaignDetails: { Source: string } };
@@ -86,3 +88,40 @@ export const getCampaignSources = (): CampaignSources => {
86
88
  yardiCampaignSource,
87
89
  };
88
90
  };
91
+
92
+ export enum LogType {
93
+ error = "error",
94
+ info = "info",
95
+ warn = "warn",
96
+ }
97
+
98
+ export const sendLoggingEvent = async ({
99
+ logType,
100
+ buildingSlug,
101
+ orgSlug,
102
+ logTitle,
103
+ logData,
104
+ }: {
105
+ logType?: LogType;
106
+ buildingSlug?: string;
107
+ orgSlug?: string;
108
+ logTitle: string;
109
+ logData: Record<string, unknown>;
110
+ }): Promise<void> => {
111
+ const host = "https://app.meetelise.com";
112
+ await axios.post(
113
+ `${host}/platformApi/webchat/logging`,
114
+ {
115
+ ...logData,
116
+ },
117
+ {
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ "building-slug": buildingSlug ?? "",
121
+ "org-slug": orgSlug ?? "",
122
+ "log-type": logType ?? LogType.error,
123
+ "log-title": logTitle.toUpperCase().replace(/\s/g, "_"),
124
+ },
125
+ }
126
+ );
127
+ };
@@ -1,3 +1,5 @@
1
+ import axios from "axios";
2
+
1
3
  export interface LabeledOption {
2
4
  label: string;
3
5
  value: string | number;
@@ -49,20 +51,19 @@ export default async function fetchBuildingInfo(
49
51
  buildingSlug: string
50
52
  ): Promise<Building> {
51
53
  const host = "https://app.meetelise.com";
52
- const url = `${host}/api/pub/v1/organization/${orgSlug}/building/${buildingSlug}`;
53
- const response = await fetch(url);
54
- const building: Building = await response.json();
54
+ const buildingUrl = `${host}/api/pub/v1/organization/${orgSlug}/building/${buildingSlug}`;
55
+ const unitsUrl = `${host}/eliseCrmApi/pub/building/${buildingSlug}/units`;
56
+ const layoutsUrl = `${host}/eliseCrmApi/pub/building/${buildingSlug}/layouts`;
57
+
58
+ const [buildingResponse, unitsResponse, layoutsResponse] = await Promise.all([
59
+ axios.get(buildingUrl),
60
+ axios.get(unitsUrl),
61
+ axios.get(layoutsUrl),
62
+ ]);
55
63
 
56
- // HACK
57
- // We will fetch these units/layouts from elise-crm-api and supplement the DTO
58
- const unitsResponse = await fetch(
59
- `${host}/eliseCrmApi/pub/building/${buildingSlug}/units?showAvailableOnly=true`
60
- );
61
- const units: UnitV2[] = await unitsResponse.json();
62
- const layoutsResponse = await fetch(
63
- `${host}/eliseCrmApi/pub/building/${buildingSlug}/layouts`
64
- );
65
- const layouts: string[] = await layoutsResponse.json();
64
+ const building: Building = buildingResponse.data;
65
+ const units: UnitV2[] = unitsResponse.data;
66
+ const layouts: string[] = layoutsResponse.data;
66
67
 
67
68
  building.unitOptionsV2 = units;
68
69
  building.layoutOptionsV2 = layouts;