@meetelise/chat 1.49.0 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/MyPubnub.ts CHANGED
@@ -7,10 +7,6 @@ import { ChatStorageKey, createChatStorageKey } from "./handleChatId";
7
7
  import LeadSourceClient from "./WebComponent/LeadSourceClient";
8
8
  import { pushGtmEvent } from "./gtm";
9
9
  import { isContainingEmail, snakify } from "./utils";
10
- import {
11
- collectClientContext,
12
- WebchatClientContext,
13
- } from "./collectClientContext";
14
10
  import { WidgetType } from "./main/MEChat";
15
11
  import { BuildingWebchatView } from "./fetchBuildingWebchatView";
16
12
  import { LeadSourceMultitouchClient } from "./WebComponent/LeadSourceMultitouchClient";
@@ -222,15 +218,9 @@ class MyPubnub {
222
218
  expires_in_seconds: number;
223
219
  } | null> {
224
220
  try {
225
- const payload: {
226
- building_slug: string;
227
- org_slug: string;
228
- lead_id?: string;
229
- client_context?: WebchatClientContext;
230
- } = {
221
+ const payload: Record<string, string> = {
231
222
  building_slug: buildingSlug,
232
223
  org_slug: orgSlug,
233
- client_context: collectClientContext(),
234
224
  };
235
225
  if (existingLeadId) {
236
226
  payload.lead_id = existingLeadId;
@@ -187,6 +187,9 @@ export class TourScheduler extends LitElement {
187
187
  @property({ attribute: true })
188
188
  orgLegalName = "";
189
189
 
190
+ @property({ attribute: false })
191
+ country: string | null = null;
192
+
190
193
  @property({ attribute: true })
191
194
  currentLeadSource = ""; // the default lead source based on referrer and query params.
192
195
 
@@ -1771,6 +1774,7 @@ export class TourScheduler extends LitElement {
1771
1774
  emailInput: this.emailInput?.value,
1772
1775
  orgLegalName: this.orgLegalName,
1773
1776
  orgSlug: this.orgSlug,
1777
+ country: this.country,
1774
1778
  clickingButtonText: "schedule tour",
1775
1779
  })}
1776
1780
  </div>
@@ -1825,6 +1829,7 @@ export class TourScheduler extends LitElement {
1825
1829
  emailInput: this.emailInput?.value,
1826
1830
  orgLegalName: this.orgLegalName,
1827
1831
  orgSlug: this.orgSlug,
1832
+ country: this.country,
1828
1833
  clickingButtonText: "schedule tour",
1829
1834
  })}
1830
1835
  </div>
@@ -1934,6 +1939,7 @@ export class TourScheduler extends LitElement {
1934
1939
  emailInput: this.emailInput?.value,
1935
1940
  orgLegalName: this.orgLegalName,
1936
1941
  orgSlug: this.orgSlug,
1942
+ country: this.country,
1937
1943
  clickingButtonText: "schedule tour",
1938
1944
  })}
1939
1945
  `
@@ -22,6 +22,10 @@ import {
22
22
  isNumericInput,
23
23
  } from "./formatPhoneNumber";
24
24
  import { InputStyles } from "./InputStyles";
25
+ import {
26
+ CANADA_PRIVACY_POLICY_URL,
27
+ isCanadianBuilding,
28
+ } from "../../disclaimers";
25
29
 
26
30
  @customElement("call-us-window")
27
31
  export class CallUsWindow extends LitElement {
@@ -167,6 +171,8 @@ export class CallUsWindow extends LitElement {
167
171
 
168
172
  @property({ attribute: true })
169
173
  orgLegalName = "";
174
+ @property({ attribute: false })
175
+ country: string | null = null;
170
176
 
171
177
  @property({ attribute: true })
172
178
  private leadSourceClient: LeadSourceClient | null = null;
@@ -315,7 +321,21 @@ export class CallUsWindow extends LitElement {
315
321
  </div>
316
322
 
317
323
  <div class="text-us-window__description">
318
- ${this.buildingId === 196098 // Liss Lawndale Apartments
324
+ ${isCanadianBuilding(this.country)
325
+ ? html`By providing your number and clicking send, you consent to
326
+ receive recurring commercial electronic messages from or on behalf
327
+ of ${this.orgLegalName} at this number. Messages may be AI or
328
+ human generated. Msg & Data rates may apply. You consent to this
329
+ <a
330
+ target="_blank"
331
+ rel="noopener noreferrer"
332
+ href=${CANADA_PRIVACY_POLICY_URL}
333
+ >privacy policy</a
334
+ >, including having your number and communications recorded and
335
+ used by our service provider to generate responses and improve
336
+ their services. You may withdraw your consent at any time by
337
+ unsubscribing or contacting us.`
338
+ : this.buildingId === 196098 // Liss Lawndale Apartments
319
339
  ? html`By providing your number and clicking send, you consent to
320
340
  receive recurring marketing calls and voice and text messages from
321
341
  or on behalf of ${this.orgLegalName} at this number using
@@ -123,6 +123,8 @@ export class EmailUsWindow extends LitElement {
123
123
  buildingName = "";
124
124
  @property({ attribute: true })
125
125
  orgLegalName = "";
126
+ @property({ attribute: false })
127
+ country: string | null = null;
126
128
 
127
129
  @property({ attribute: true })
128
130
  private leadSourceClient: LeadSourceClient | null = null;
@@ -507,6 +509,7 @@ export class EmailUsWindow extends LitElement {
507
509
  emailInput: this.email,
508
510
  orgLegalName: this.orgLegalName,
509
511
  orgSlug: this.orgSlug,
512
+ country: this.country,
510
513
  clickingButtonText: "send",
511
514
  })}
512
515
  </details-window>
@@ -76,6 +76,8 @@ export class Launcher extends LitElement {
76
76
  chatId = "";
77
77
  @property({ attribute: true })
78
78
  orgLegalName = "";
79
+ @property({ attribute: false })
80
+ country: string | null = null;
79
81
  @property({ attribute: true })
80
82
  chatCallUsHeader = "";
81
83
  @property({ attribute: false })
@@ -1281,6 +1283,7 @@ export class Launcher extends LitElement {
1281
1283
  .leadSourceClient=${this.leadSourceClient}
1282
1284
  .leadSourceMultitouchClient=${this.leadSourceMultitouchClient}
1283
1285
  .orgLegalName=${this.orgLegalName}
1286
+ .country=${this.country}
1284
1287
  >
1285
1288
  </email-us-window>
1286
1289
  </div>`
@@ -1308,6 +1311,7 @@ export class Launcher extends LitElement {
1308
1311
  .tourTypeOptions=${this.tourTypeOptions}
1309
1312
  .layoutOptions=${this.layoutOptions}
1310
1313
  .orgLegalName=${this.orgLegalName}
1314
+ .country=${this.country}
1311
1315
  buildingId=${this.buildingId}
1312
1316
  orgId=${this.orgId}
1313
1317
  .leadSourceParams=${this.leadSourceParams}
@@ -1354,6 +1358,7 @@ export class Launcher extends LitElement {
1354
1358
  .leadSourceClient=${this.leadSourceClient}
1355
1359
  .leadSourceMultitouchClient=${this.leadSourceMultitouchClient}
1356
1360
  .orgLegalName=${this.orgLegalName}
1361
+ .country=${this.country}
1357
1362
  ></call-us-window>
1358
1363
  </div>
1359
1364
  `
@@ -7,6 +7,7 @@ import "./Scheduler/tour-scheduler";
7
7
  import fetchBuildingWebchatView, {
8
8
  BuildingWebchatView,
9
9
  DesignConcepts,
10
+ fetchOrgCentralWebchatView,
10
11
  } from "../fetchBuildingWebchatView";
11
12
  import Analytics, { logSignal, LogType, sendLoggingEvent } from "../analytics";
12
13
  import {
@@ -126,6 +127,8 @@ export class MEChat extends LitElement {
126
127
  private buildingSlug = "";
127
128
  @property({ type: String })
128
129
  private orgSlug = "";
130
+ @property({ type: Boolean })
131
+ private centralConvo = false;
129
132
  @property({ type: Object })
130
133
  launcherStyles: StyleInfo = {};
131
134
  @property()
@@ -265,11 +268,11 @@ export class MEChat extends LitElement {
265
268
  firstUpdated = async (): Promise<void> => this.setupWebchat();
266
269
 
267
270
  private setupWebchat = async (): Promise<void> => {
268
- if (!this.buildingSlug || !this.orgSlug) return;
269
- const buildingWebchatView = await fetchBuildingWebchatView(
270
- this.orgSlug,
271
- this.buildingSlug
272
- );
271
+ if (!this.orgSlug) return;
272
+ if (!this.centralConvo && !this.buildingSlug) return;
273
+ const buildingWebchatView = this.centralConvo
274
+ ? await fetchOrgCentralWebchatView(this.orgSlug)
275
+ : await fetchBuildingWebchatView(this.orgSlug, this.buildingSlug);
273
276
 
274
277
  if (Math.random() < 0.2) {
275
278
  try {
@@ -1009,6 +1012,7 @@ export class MEChat extends LitElement {
1009
1012
  .hasDynamicSchedulingEnabled=${this.buildingWebchatView
1010
1013
  ?.usesDynamicScheduling ?? false}
1011
1014
  .orgLegalName=${this.buildingWebchatView?.orgLegalName ?? ""}
1015
+ .country=${this.buildingWebchatView?.country ?? null}
1012
1016
  .tourTypeOptions=${this.buildingWebchatView
1013
1017
  ?.tourTypeOptions ?? []}
1014
1018
  .layoutOptions=${this.buildingWebchatView?.layoutOptions ??
@@ -686,6 +686,7 @@ export class PubnubChat extends LitElement {
686
686
  ${inChatThreadDisclaimer({
687
687
  orgSlug: this.orgSlug ?? "",
688
688
  buildingState: this.building?.state ?? null,
689
+ buildingCountry: this.building?.country ?? null,
689
690
  type: "leasing",
690
691
  customAdditionalDisclaimer:
691
692
  this.building?.customAdditionalDisclaimer,
@@ -17,6 +17,12 @@ const disclaimerStyles = {
17
17
  // States where we need to say "you are interacting with generative AI".
18
18
  const GENERATIVE_AI_DISCLAIMER_STATES = ["UT", "ME"];
19
19
 
20
+ export const CANADA_PRIVACY_POLICY_URL = "https://eliseai.com/canada-policy";
21
+
22
+ export const isCanadianBuilding = (
23
+ country: string | null | undefined
24
+ ): boolean => country === "CA";
25
+
20
26
  export const CURRENT_CONSENT_ID = "currentConsentId";
21
27
 
22
28
  const formDisclaimer = ({
@@ -24,6 +30,7 @@ const formDisclaimer = ({
24
30
  emailInput,
25
31
  orgLegalName,
26
32
  orgSlug,
33
+ country,
27
34
  clickingButtonText = "send",
28
35
  }: {
29
36
  buildingName: string;
@@ -31,8 +38,13 @@ const formDisclaimer = ({
31
38
  emailInput?: string;
32
39
  orgLegalName: string;
33
40
  orgSlug: string;
41
+ country?: string | null;
34
42
  clickingButtonText?: string;
35
43
  }): TemplateResult => {
44
+ const policyUrl = isCanadianBuilding(country)
45
+ ? CANADA_PRIVACY_POLICY_URL
46
+ : "http://bit.ly/me_privacy_policy";
47
+
36
48
  if (phoneNumberInput && orgSlug === "greystar") {
37
49
  return html`<div style=${styleMap(disclaimerStyles.container)}>
38
50
  By providing your phone number and clicking ${clickingButtonText}, you
@@ -70,7 +82,7 @@ const formDisclaimer = ({
70
82
  consent to this
71
83
  <a
72
84
  style=${styleMap(disclaimerStyles.link)}
73
- href="http://bit.ly/me_privacy_policy"
85
+ href=${policyUrl}
74
86
  target="_blank"
75
87
  rel="noopener noreferrer"
76
88
  >privacy policy</a
@@ -84,7 +96,7 @@ const formDisclaimer = ({
84
96
  this
85
97
  <a
86
98
  style=${styleMap(disclaimerStyles.link)}
87
- href="http://bit.ly/me_privacy_policy"
99
+ href=${policyUrl}
88
100
  target="_blank"
89
101
  rel="noopener noreferrer"
90
102
  >privacy policy</a
@@ -93,12 +105,12 @@ const formDisclaimer = ({
93
105
  </div>`;
94
106
  }
95
107
  // Default: no email or phone entered yet. Still surface the privacy policy
96
- // link so it's discoverable in the tab order before the user types.
108
+ // link so its discoverable in the tab order before the user types.
97
109
  return html` <div style=${styleMap(disclaimerStyles.container)}>
98
110
  By clicking ${clickingButtonText}, you consent to this
99
111
  <a
100
112
  style=${styleMap(disclaimerStyles.link)}
101
- href="http://bit.ly/me_privacy_policy"
113
+ href=${policyUrl}
102
114
  target="_blank"
103
115
  rel="noopener noreferrer"
104
116
  >privacy policy</a
@@ -232,15 +244,19 @@ export const getConsentPreChatDisclaimer = (
232
244
  export const inChatThreadDisclaimer = ({
233
245
  orgSlug,
234
246
  buildingState,
247
+ buildingCountry,
235
248
  type,
236
249
  customAdditionalDisclaimer,
237
250
  }: {
238
251
  orgSlug: string;
239
252
  buildingState: string | null;
253
+ buildingCountry?: string | null;
240
254
  type: "healthcare" | "utilities" | "leasing";
241
255
  customAdditionalDisclaimer?: string | null;
242
256
  }): TemplateResult => {
243
- const privacyPolicyUrl = getPrivacyPolicyUrl(orgSlug);
257
+ const privacyPolicyUrl = isCanadianBuilding(buildingCountry)
258
+ ? CANADA_PRIVACY_POLICY_URL
259
+ : getPrivacyPolicyUrl(orgSlug);
244
260
 
245
261
  const safeAdditionalDisclaimer = customAdditionalDisclaimer
246
262
  ? html`<br />${unsafeHTML(customAdditionalDisclaimer)}`
@@ -300,6 +316,26 @@ export const inChatThreadDisclaimer = ({
300
316
  `;
301
317
  }
302
318
 
319
+ if (isCanadianBuilding(buildingCountry)) {
320
+ return html`
321
+ <li class="disclaimer-container disclaimer-message">
322
+ <p class="disclaimer-inner">
323
+ By interacting with this system, you consent to
324
+ <a
325
+ href=${privacyPolicyUrl}
326
+ target="_blank"
327
+ rel="noopener noreferrer"
328
+ >
329
+ this privacy policy</a
330
+ >, including being contacted by AI or a human & having your
331
+ conversation recorded and used by a third-party service provider to
332
+ generate responses and improve their services.
333
+ ${safeAdditionalDisclaimer}
334
+ </p>
335
+ </li>
336
+ `;
337
+ }
338
+
303
339
  return html`
304
340
  <li class="disclaimer-container disclaimer-message">
305
341
  <p class="disclaimer-inner">
@@ -131,6 +131,7 @@ export interface BuildingWebchatView {
131
131
  featureFlagWebchatReplaceAnyScheduleTourCtaWebsite: boolean;
132
132
  customAdditionalDisclaimer: string | null;
133
133
  isAip: boolean;
134
+ country: string | null;
134
135
  }
135
136
 
136
137
  /**
@@ -147,20 +148,40 @@ export default async function fetchBuildingWebchatView(
147
148
  const host = "https://app.meetelise.com";
148
149
  const buildingUrl = `${host}/api/pub/v1/organization/${orgSlug}/building/${buildingSlug}/v2`;
149
150
  const buildingResponse = await axios.get(buildingUrl);
151
+ return withReplacedWelcomeName(buildingResponse.data);
152
+ }
150
153
 
151
- const buildingWebchatView: BuildingWebchatView = buildingResponse.data;
154
+ /**
155
+ * Load the org-anchored webchat config for centralized webchat, where the widget
156
+ * is scoped to the organization rather than a single building. The building/group
157
+ * slug on the page is used only for navigation-event tracking, not for config.
158
+ *
159
+ * @param orgSlug - The org slug, e.g. "big-prop-co"
160
+ * @returns The org-level webchat config, in the same shape as a building's.
161
+ */
162
+ export async function fetchOrgCentralWebchatView(
163
+ orgSlug: string
164
+ ): Promise<BuildingWebchatView> {
165
+ const host = "https://app.meetelise.com";
166
+ const orgUrl = `${host}/api/pub/v1/organization/${orgSlug}/webchat/v2`;
167
+ const orgResponse = await axios.get(orgUrl);
168
+ return withReplacedWelcomeName(orgResponse.data);
169
+ }
152
170
 
171
+ const withReplacedWelcomeName = (
172
+ view: BuildingWebchatView
173
+ ): BuildingWebchatView => {
153
174
  try {
154
- buildingWebchatView.welcomeMessage = replaceName(
155
- buildingWebchatView.welcomeMessage ?? "",
156
- buildingWebchatView.userFirstName
175
+ view.welcomeMessage = replaceName(
176
+ view.welcomeMessage ?? "",
177
+ view.userFirstName
157
178
  );
158
179
  } catch (e) {
159
180
  // eslint-disable-next-line no-console
160
181
  console.error(e);
161
182
  }
162
- return buildingWebchatView;
163
- }
183
+ return view;
184
+ };
164
185
 
165
186
  const replaceName = (originalText: string, newName: string) => {
166
187
  const regex = /elise/gi;
@@ -205,12 +205,18 @@ export default class MEChat {
205
205
  }
206
206
 
207
207
  if (meChatElement instanceof MEChatLitElement) {
208
- this.handleBuildingslug(
209
- meChatElement as MEChatLitElement,
210
- opts.organization,
211
- opts.building,
212
- opts
213
- );
208
+ if (opts.centralConvo) {
209
+ // Centralized webchat is org-anchored: config comes from the org, not a
210
+ // per-building slug or site resolution.
211
+ meChatElement.setAttribute("centralConvo", "true");
212
+ } else {
213
+ this.handleBuildingslug(
214
+ meChatElement as MEChatLitElement,
215
+ opts.organization,
216
+ opts.building,
217
+ opts
218
+ );
219
+ }
214
220
  if (opts.launcherStyles) {
215
221
  meChatElement.launcherStyles = opts.launcherStyles;
216
222
  }
@@ -536,6 +542,7 @@ function coerceStudioSessionScopeKey(
536
542
  export interface Options {
537
543
  building?: string;
538
544
  organization?: string;
545
+ centralConvo?: boolean;
539
546
  studio?: StudioWebchatOptions;
540
547
  avatarSrc?: string;
541
548
  mini?: boolean;
@@ -1,12 +0,0 @@
1
- export interface WebchatClientContext {
2
- page_url?: string;
3
- referrer?: string;
4
- document_title?: string;
5
- screen_width?: number;
6
- screen_height?: number;
7
- device_pixel_ratio?: number;
8
- timezone?: string;
9
- campaign_sources?: string;
10
- ga_client_id?: string;
11
- }
12
- export declare const collectClientContext: () => WebchatClientContext;
@@ -1,42 +0,0 @@
1
- import { getCampaignSources } from "./analytics";
2
- import { getCookieValue } from "./utils";
3
-
4
- export interface WebchatClientContext {
5
- page_url?: string;
6
- referrer?: string;
7
- document_title?: string;
8
- screen_width?: number;
9
- screen_height?: number;
10
- device_pixel_ratio?: number;
11
- timezone?: string;
12
- campaign_sources?: string;
13
- ga_client_id?: string;
14
- }
15
-
16
- export const collectClientContext = (): WebchatClientContext => {
17
- const context: WebchatClientContext = {};
18
- try {
19
- context.page_url = window.location.href;
20
- if (document.referrer) {
21
- context.referrer = document.referrer;
22
- }
23
- if (document.title) {
24
- context.document_title = document.title;
25
- }
26
- if (window.screen) {
27
- context.screen_width = window.screen.width;
28
- context.screen_height = window.screen.height;
29
- }
30
- context.device_pixel_ratio = window.devicePixelRatio;
31
- context.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
32
- context.campaign_sources = JSON.stringify(getCampaignSources());
33
- const gaClientId = getCookieValue("_ga");
34
- if (gaClientId) {
35
- context.ga_client_id = gaClientId;
36
- }
37
- } catch (e) {
38
- // eslint-disable-next-line no-console
39
- console.warn(`Couldnt collect webchat client context, error: ${e}`);
40
- }
41
- return context;
42
- };