@meetelise/chat 1.51.13 → 1.51.15

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.
@@ -325,7 +325,8 @@ export class CallUsWindow extends LitElement {
325
325
  ? html`By providing your number and clicking send, you consent to
326
326
  receive recurring commercial electronic messages from or on behalf
327
327
  of ${this.orgLegalName} at this number. Messages may be AI or
328
- human generated. Msg & Data rates may apply. You consent to this
328
+ human generated. Msg freq varies. Msg & Data rates may apply. You
329
+ consent to this
329
330
  <a
330
331
  target="_blank"
331
332
  rel="noopener noreferrer"
@@ -334,28 +335,30 @@ export class CallUsWindow extends LitElement {
334
335
  >, including having your number and communications recorded and
335
336
  used by our service provider to generate responses and improve
336
337
  their services. You may withdraw your consent at any time by
337
- unsubscribing or contacting us.`
338
+ unsubscribing or contacting us. Text STOP to opt-out of messages.
339
+ Text HELP for assistance.`
338
340
  : this.buildingId === 196098 // Liss Lawndale Apartments
339
341
  ? html`By providing your number and clicking send, you consent to
340
- receive recurring marketing calls and voice and text messages from
342
+ receive recurring marketing text and voice messages and calls from
341
343
  or on behalf of ${this.orgLegalName} at this number using
342
344
  artificial voice or an autodialer system. Messages may be AI or
343
345
  human generated. This consent is not required to lease at this
344
- property. Message frequency varies. Msg & Data rates may apply.
345
- Reply STOP to opt-out or HELP for help. You consent to this
346
+ property. Msg freq varies. Msg & Data rates may apply. Text STOP
347
+ to opt-out of messages. Text HELP for assistance. You consent to
348
+ this
346
349
  <a target="_blank" href="http://bit.ly/me_privacy_policy"
347
350
  >privacy policy</a
348
351
  >, including having your number and communications recorded and
349
352
  used by a third party.`
350
353
  : this.orgSlug === "greystar"
351
354
  ? html`By providing your phone number and clicking send, you consent
352
- to receive recurring marketing calls and voice and text messages
355
+ to receive recurring marketing text and voice messages and calls
353
356
  from Greystar or on its behalf at this number using artificial
354
357
  voice and/or an autodialer. Messages may be AI or human-generated.
355
358
  Your phone number and communications are shared with and recorded
356
359
  and used by EliseAI. You are not required to provide your phone
357
- number or consent to these terms to lease at this property. Msg &
358
- Data rates may apply. You consent to
360
+ number or consent to these terms to lease at this property. Msg
361
+ freq varies. Msg & Data rates may apply. You consent to
359
362
  <a
360
363
  href="https://www.greystar.com/privacy"
361
364
  target="_blank"
@@ -368,13 +371,14 @@ export class CallUsWindow extends LitElement {
368
371
  target="_blank"
369
372
  rel="noopener noreferrer"
370
373
  >EliseAI's Privacy Policy</a
371
- >.`
374
+ >. Text STOP to opt-out of messages. Text HELP for assistance.`
372
375
  : html`By providing your number and clicking send, you consent to
373
- receive recurring marketing calls and voice and text messages from
376
+ receive recurring marketing text and voice messages and calls from
374
377
  or on behalf of ${this.orgLegalName} at this number using
375
378
  artificial voice or an autodialer system. Messages may be AI or
376
379
  human generated. This consent is not required to lease at this
377
- property. Msg & Data rates may apply. You consent to this
380
+ property. Msg freq varies. Msg & Data rates may apply. You consent
381
+ to this
378
382
  <a target="_blank" href="http://bit.ly/me_privacy_policy"
379
383
  >privacy policy</a
380
384
  >
@@ -382,7 +386,8 @@ export class CallUsWindow extends LitElement {
382
386
  <a target="_blank" href="https://eliseai.com/msgterms"
383
387
  >terms and conditions</a
384
388
  >, including having your number and communications recorded and
385
- used by a third party.`}
389
+ used by a third party. Text STOP to opt-out of messages. Text HELP
390
+ for assistance.`}
386
391
  </div>
387
392
  </div>`;
388
393
  };
@@ -424,7 +424,12 @@ export class MEChat extends LitElement {
424
424
  this.enabledChatWidgets.sstMobile ||
425
425
  this.orgSlug === "radco"
426
426
  ) {
427
- replaceSSTLinkWithTourWidgetTrigger(this.handleTourClicked);
427
+ replaceSSTLinkWithTourWidgetTrigger(
428
+ this.handleTourClicked,
429
+ this.orgSlug,
430
+ this.buildingSlug,
431
+ () => this.isConnected
432
+ );
428
433
  if (featureFlagReplaceScheduleTourCtaWebsite) {
429
434
  replacePageRedirectElementWithTourWidgetTrigger(this.handleTourClicked);
430
435
  }
@@ -63,6 +63,10 @@ const formDisclaimer = ({
63
63
  recorded and used by our service provider EliseAI to generate responses
64
64
  and improve their services. You may withdraw your consent at any time by
65
65
  unsubscribing or contacting us.
66
+ ${phoneNumberInput
67
+ ? html`Msg freq varies. Text STOP to opt-out of messages. Text HELP
68
+ for assistance.`
69
+ : ""}
66
70
  </div>`;
67
71
  }
68
72
 
@@ -81,19 +85,23 @@ const formDisclaimer = ({
81
85
  recorded and used by our service provider EliseAI to generate responses
82
86
  and improve their services. You may withdraw your consent at any time by
83
87
  unsubscribing or contacting us.
88
+ ${phoneNumberInput
89
+ ? html`Msg freq varies. Text STOP to opt-out of messages. Text HELP for
90
+ assistance.`
91
+ : ""}
84
92
  </div>`;
85
93
  }
86
94
 
87
95
  if (phoneNumberInput && orgSlug === "greystar") {
88
96
  return html`<div style=${styleMap(disclaimerStyles.container)}>
89
97
  By providing your phone number and clicking ${clickingButtonText}, you
90
- consent to receive recurring marketing calls and voice and text messages
98
+ consent to receive recurring marketing text and voice messages and calls
91
99
  from Greystar or on its behalf at this number using artificial voice
92
100
  and/or an autodialer. Messages may be AI or human-generated. Your phone
93
101
  number and communications are shared with and recorded and used by
94
102
  EliseAI. You are not required to provide your phone number or consent to
95
- these terms to lease at this property. Msg & Data rates may apply. You
96
- consent to
103
+ these terms to lease at this property. Msg freq varies. Msg & Data rates
104
+ may apply. You consent to
97
105
  <a
98
106
  style=${styleMap(disclaimerStyles.link)}
99
107
  href="https://www.greystar.com/privacy"
@@ -108,17 +116,17 @@ const formDisclaimer = ({
108
116
  target="_blank"
109
117
  rel="noopener noreferrer"
110
118
  >EliseAI’s Privacy Policy</a
111
- >.
119
+ >. Text STOP to opt-out of messages. Text HELP for assistance.
112
120
  </div>`;
113
121
  }
114
122
  if (phoneNumberInput) {
115
123
  return html` <div style=${styleMap(disclaimerStyles.container)}>
116
124
  By providing your number and clicking ${clickingButtonText}, you consent
117
- to receive recurring marketing calls and voice and text messages from or
125
+ to receive recurring marketing text and voice messages and calls from or
118
126
  on behalf of ${orgLegalName} at this number using artificial voice or an
119
127
  autodialer system. Messages may be AI or human generated. This consent is
120
- not required to lease at this property. Msg & Data rates may apply. You
121
- consent to this
128
+ not required to lease at this property. Msg freq varies. Msg & Data rates
129
+ may apply. You consent to this
122
130
  <a
123
131
  style=${styleMap(disclaimerStyles.link)}
124
132
  href=${policyUrl}
@@ -126,7 +134,7 @@ const formDisclaimer = ({
126
134
  rel="noopener noreferrer"
127
135
  >privacy policy</a
128
136
  >, including having your number and communications recorded and used by a
129
- third party.
137
+ third party. Text STOP to opt-out of messages. Text HELP for assistance.
130
138
  </div>`;
131
139
  }
132
140
  if (emailInput) {
@@ -394,6 +402,7 @@ export const inChatThreadDisclaimer = ({
394
402
  </li>
395
403
  `;
396
404
  }
405
+
397
406
  return html``;
398
407
  };
399
408
 
@@ -0,0 +1,216 @@
1
+ import { expect } from "@esm-bundle/chai";
2
+ import {
3
+ replacePageRedirectElementWithTourWidgetTrigger,
4
+ replaceSSTLinkWithTourWidgetTrigger,
5
+ replaceTourTriggerElementWithTourWidgetTrigger,
6
+ } from "./insertLeadSourceIntoSchedulerLinks";
7
+
8
+ const APP = "https://app.meetelise.com";
9
+ const ORG = "test-org";
10
+ const BUILDING = "test-building";
11
+
12
+ const render = (hrefs: string[]): HTMLAnchorElement[] => {
13
+ document.body.innerHTML = hrefs
14
+ .map((href) => `<a href="${href}">link</a>`)
15
+ .join("");
16
+ return Array.from(document.querySelectorAll("a"));
17
+ };
18
+
19
+ // Returns whether the widget cancelled the click. The document listener runs
20
+ // after the link's own listeners, then cancels navigation so the test page stays.
21
+ const click = (element: Element): boolean => {
22
+ let defaultPrevented = false;
23
+ const recordAndCancel = (event: Event) => {
24
+ defaultPrevented = event.defaultPrevented;
25
+ event.preventDefault();
26
+ };
27
+ document.addEventListener("click", recordAndCancel);
28
+ element.dispatchEvent(
29
+ new MouseEvent("click", { bubbles: true, cancelable: true })
30
+ );
31
+ document.removeEventListener("click", recordAndCancel);
32
+ return defaultPrevented;
33
+ };
34
+
35
+ const clickAll = (links: HTMLAnchorElement[]): Record<string, boolean> =>
36
+ Object.fromEntries(
37
+ links.map((link) => [link.getAttribute("href") ?? "", click(link)])
38
+ );
39
+
40
+ const allEqual = (hrefs: string[], value: boolean): Record<string, boolean> =>
41
+ Object.fromEntries(hrefs.map((href) => [href, value]));
42
+
43
+ describe("replaceSSTLinkWithTourWidgetTrigger", () => {
44
+ let opened: MouseEvent[];
45
+ const install = (orgSlug = ORG, buildingSlug = BUILDING): void =>
46
+ replaceSSTLinkWithTourWidgetTrigger(
47
+ (event) => opened.push(event),
48
+ orgSlug,
49
+ buildingSlug,
50
+ () => true
51
+ );
52
+
53
+ beforeEach(() => {
54
+ opened = [];
55
+ });
56
+
57
+ afterEach(() => {
58
+ document.body.innerHTML = "";
59
+ });
60
+
61
+ it("opens the scheduler only for the current building's tour link on a portfolio page", () => {
62
+ const [ownTour, otherTour, contactUs] = render([
63
+ `${APP}/${ORG}/${BUILDING}/tour`,
64
+ `${APP}/${ORG}/other-building/tour`,
65
+ `${APP}/${ORG}/${BUILDING}/contactUs`,
66
+ ]);
67
+
68
+ install();
69
+
70
+ expect({
71
+ ownTour: click(ownTour),
72
+ otherTour: click(otherTour),
73
+ contactUs: click(contactUs),
74
+ }).to.deep.equal({ ownTour: true, otherTour: false, contactUs: false });
75
+ expect(opened).to.have.length(1);
76
+ });
77
+
78
+ it("matches the current building's tour link with query strings, fragments, trailing slashes and any casing", () => {
79
+ const hrefs = [
80
+ `${APP}/${ORG}/${BUILDING}/tour/`,
81
+ `${APP}/${ORG}/${BUILDING}/tour?utm_source=website`,
82
+ `${APP}/${ORG}/${BUILDING}/tour/?hideScrollBars=true`,
83
+ `${APP}/${ORG}/${BUILDING}/tour?stateId=abc123`,
84
+ `${APP}/${ORG}/${BUILDING}/tour#schedule`,
85
+ `${APP}/${ORG}//${BUILDING}/tour`,
86
+ `${APP}/Test-Org/Test-Building/Tour`,
87
+ ];
88
+ const links = render(hrefs);
89
+
90
+ install();
91
+
92
+ expect(clickAll(links)).to.deep.equal(allEqual(hrefs, true));
93
+ expect(opened).to.have.length(hrefs.length);
94
+ });
95
+
96
+ it("matches when the widget was started with differently cased slugs", () => {
97
+ const [link] = render([`${APP}/${ORG}/${BUILDING}/tour`]);
98
+
99
+ install("Test-Org", "TEST-BUILDING");
100
+
101
+ expect(click(link)).to.equal(true);
102
+ });
103
+
104
+ it("keeps default navigation for every other app.meetelise.com link", () => {
105
+ const hrefs = [
106
+ `${APP}/other-org/${BUILDING}/tour`,
107
+ `${APP}/${ORG}/${BUILDING}-2/tour`,
108
+ `${APP}/${ORG}/${BUILDING}/tour-2`,
109
+ `${APP}/${ORG}/${BUILDING}/tour/extra`,
110
+ `${APP}/${ORG}/${BUILDING}/contactUs?utm_source=website`,
111
+ `${APP}/${ORG}/${BUILDING}`,
112
+ `${APP}/${ORG}/tour`,
113
+ `${APP}/${BUILDING}/tour`,
114
+ `${APP}/settings/widgets`,
115
+ `${APP}/`,
116
+ ];
117
+ const links = render(hrefs);
118
+
119
+ install();
120
+
121
+ expect(clickAll(links)).to.deep.equal(allEqual(hrefs, false));
122
+ expect(opened).to.have.length(0);
123
+ });
124
+
125
+ it("never intercepts links outside https://app.meetelise.com", () => {
126
+ const hrefs = [
127
+ `/${ORG}/${BUILDING}/tour`,
128
+ `http://app.meetelise.com/${ORG}/${BUILDING}/tour`,
129
+ `https://www.example.com/${ORG}/${BUILDING}/tour`,
130
+ ];
131
+ const links = render(hrefs);
132
+
133
+ install();
134
+
135
+ expect(clickAll(links)).to.deep.equal(allEqual(hrefs, false));
136
+ });
137
+
138
+ it("lets a removed widget's tour link navigate after the widget switches buildings", () => {
139
+ const [firstTour, secondTour] = render([
140
+ `${APP}/${ORG}/${BUILDING}/tour`,
141
+ `${APP}/${ORG}/other-building/tour`,
142
+ ]);
143
+ const openedByFirst: MouseEvent[] = [];
144
+ const openedBySecond: MouseEvent[] = [];
145
+ let firstMounted = true;
146
+ replaceSSTLinkWithTourWidgetTrigger(
147
+ (event) => openedByFirst.push(event),
148
+ ORG,
149
+ BUILDING,
150
+ () => firstMounted
151
+ );
152
+
153
+ // Studio removes the first widget and starts one for another building.
154
+ firstMounted = false;
155
+ replaceSSTLinkWithTourWidgetTrigger(
156
+ (event) => openedBySecond.push(event),
157
+ ORG,
158
+ "other-building",
159
+ () => true
160
+ );
161
+
162
+ expect({
163
+ firstTour: click(firstTour),
164
+ secondTour: click(secondTour),
165
+ }).to.deep.equal({ firstTour: false, secondTour: true });
166
+ expect(openedByFirst).to.have.length(0);
167
+ expect(openedBySecond).to.have.length(1);
168
+ });
169
+
170
+ it("intercepts nothing when the building slug is missing", () => {
171
+ const hrefs = [`${APP}/${ORG}/${BUILDING}/tour`, `${APP}/${ORG}//tour`];
172
+ const links = render(hrefs);
173
+
174
+ install(ORG, "");
175
+
176
+ expect(clickAll(links)).to.deep.equal(allEqual(hrefs, false));
177
+ expect(opened).to.have.length(0);
178
+ });
179
+ });
180
+
181
+ describe("site schedule-a-tour hooks", () => {
182
+ let opened: MouseEvent[];
183
+
184
+ beforeEach(() => {
185
+ opened = [];
186
+ });
187
+
188
+ afterEach(() => {
189
+ document.body.innerHTML = "";
190
+ });
191
+
192
+ it("opens the scheduler from links ending in schedule-a-tour", () => {
193
+ const [link] = render(["/apartments/oh/westerville/schedule-a-tour"]);
194
+
195
+ replacePageRedirectElementWithTourWidgetTrigger((event) =>
196
+ opened.push(event)
197
+ );
198
+
199
+ expect(click(link)).to.equal(true);
200
+ expect(opened).to.have.length(1);
201
+ });
202
+
203
+ it("replaces #leaseleads-tour-open links with a scheduler trigger", () => {
204
+ render(["#leaseleads-tour-open"]);
205
+
206
+ replaceTourTriggerElementWithTourWidgetTrigger((event) =>
207
+ opened.push(event)
208
+ );
209
+
210
+ const trigger = document.querySelector("a");
211
+ expect(trigger?.hasAttribute("href")).to.equal(false);
212
+ expect(trigger?.textContent).to.equal("link");
213
+ expect(click(trigger as HTMLAnchorElement)).to.equal(true);
214
+ expect(opened).to.have.length(1);
215
+ });
216
+ });
@@ -18,14 +18,43 @@ export const insertLeadSourceIntoSchedulerLinks = (
18
18
  });
19
19
  };
20
20
 
21
+ // Mirrors the web-app's /:organization/:building/tour route, which ignores
22
+ // empty path segments. Case-insensitive so slug casing differences still match.
23
+ const isBuildingTourLink = (
24
+ href: string,
25
+ orgSlug: string,
26
+ buildingSlug: string
27
+ ): boolean => {
28
+ const [org, building, page, ...rest] = new URL(href).pathname
29
+ .split("/")
30
+ .filter(Boolean)
31
+ .map((segment) => segment.toLowerCase());
32
+ return (
33
+ rest.length === 0 &&
34
+ page === "tour" &&
35
+ org === orgSlug.toLowerCase() &&
36
+ building === buildingSlug.toLowerCase()
37
+ );
38
+ };
39
+
21
40
  export const replaceSSTLinkWithTourWidgetTrigger = (
22
- handleTourClicked: (e: MouseEvent) => void
41
+ handleTourClicked: (e: MouseEvent) => void,
42
+ orgSlug: string,
43
+ buildingSlug: string,
44
+ isWidgetMounted: () => boolean
23
45
  ): void => {
24
46
  const sstRedirectElements = document.querySelectorAll("a");
25
47
  sstRedirectElements.forEach(function (element) {
26
48
  const href = element.getAttribute("href");
27
- if (href && href.startsWith("https://app.meetelise.com/")) {
49
+ if (
50
+ href &&
51
+ href.startsWith("https://app.meetelise.com/") &&
52
+ isBuildingTourLink(href, orgSlug, buildingSlug)
53
+ ) {
28
54
  element.addEventListener("click", (event) => {
55
+ // Studio can swap the widget for another building's. A removed
56
+ // widget's listener must let the link navigate, not swallow the click.
57
+ if (!isWidgetMounted()) return;
29
58
  event.preventDefault(); // Prevents the default href redirect
30
59
  handleTourClicked(event as MouseEvent);
31
60
  });