@meetelise/chat 1.51.15 → 1.53.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.
@@ -63,6 +63,7 @@ import { getTimezoneAbbreviation } from "../../getTimezoneString";
63
63
  import startOfDay from "date-fns/startOfDay";
64
64
  import isSameDay from "date-fns/isSameDay";
65
65
  import { LeadSourceMultitouchClient } from "../LeadSourceMultitouchClient";
66
+ import { readGoogleReserveReferral } from "../../googleReserve";
66
67
  import fetchBuildingUnitsSummary, {
67
68
  UnitSummary,
68
69
  } from "../../fetchBuildingUnitsSummary";
@@ -194,6 +195,8 @@ export class TourScheduler extends LitElement {
194
195
 
195
196
  @property({ attribute: false })
196
197
  country: string | null = null;
198
+ @property({ attribute: false })
199
+ buildingState: string | null = null;
197
200
 
198
201
  @property({ attribute: true })
199
202
  currentLeadSource = ""; // the default lead source based on referrer and query params.
@@ -742,6 +745,7 @@ export class TourScheduler extends LitElement {
742
745
  unit: selectedUnit,
743
746
  conversationTrackingId: this.leadSourceClient?.chatId ?? null,
744
747
  });
748
+ const googleReferral = readGoogleReserveReferral();
745
749
  const data = {
746
750
  referrer: document.referrer,
747
751
  email_address: this.email,
@@ -774,6 +778,8 @@ export class TourScheduler extends LitElement {
774
778
  }
775
779
  ) ?? []
776
780
  ).map((source) => snakify({ ...source })),
781
+ google_rwg_token: googleReferral?.rwgToken,
782
+ google_referral_building_id: googleReferral?.buildingId,
777
783
  };
778
784
 
779
785
  const url = `https://app.meetelise.com/platformApi/state/create/v2/scheduleMe`;
@@ -1641,6 +1647,7 @@ export class TourScheduler extends LitElement {
1641
1647
  orgLegalName: this.orgLegalName,
1642
1648
  orgSlug: this.orgSlug,
1643
1649
  country: this.country,
1650
+ buildingState: this.buildingState,
1644
1651
  clickingButtonText: "schedule tour",
1645
1652
  })}
1646
1653
  </div>
@@ -1696,6 +1703,7 @@ export class TourScheduler extends LitElement {
1696
1703
  orgLegalName: this.orgLegalName,
1697
1704
  orgSlug: this.orgSlug,
1698
1705
  country: this.country,
1706
+ buildingState: this.buildingState,
1699
1707
  clickingButtonText: "schedule tour",
1700
1708
  })}
1701
1709
  </div>
@@ -1806,6 +1814,7 @@ export class TourScheduler extends LitElement {
1806
1814
  orgLegalName: this.orgLegalName,
1807
1815
  orgSlug: this.orgSlug,
1808
1816
  country: this.country,
1817
+ buildingState: this.buildingState,
1809
1818
  clickingButtonText: "schedule tour",
1810
1819
  })}
1811
1820
  `
@@ -25,6 +25,8 @@ import { InputStyles } from "./InputStyles";
25
25
  import {
26
26
  CANADA_PRIVACY_POLICY_URL,
27
27
  isCanadianBuilding,
28
+ isQuebecBuilding,
29
+ quebecConsentDisclaimer,
28
30
  } from "../../disclaimers";
29
31
 
30
32
  @customElement("call-us-window")
@@ -173,6 +175,8 @@ export class CallUsWindow extends LitElement {
173
175
  orgLegalName = "";
174
176
  @property({ attribute: false })
175
177
  country: string | null = null;
178
+ @property({ attribute: false })
179
+ buildingState: string | null = null;
176
180
 
177
181
  @property({ attribute: true })
178
182
  private leadSourceClient: LeadSourceClient | null = null;
@@ -321,7 +325,9 @@ export class CallUsWindow extends LitElement {
321
325
  </div>
322
326
 
323
327
  <div class="text-us-window__description">
324
- ${isCanadianBuilding(this.country)
328
+ ${isQuebecBuilding(this.buildingState)
329
+ ? quebecConsentDisclaimer("textUs", this.orgLegalName)
330
+ : isCanadianBuilding(this.country)
325
331
  ? html`By providing your number and clicking send, you consent to
326
332
  receive recurring commercial electronic messages from or on behalf
327
333
  of ${this.orgLegalName} at this number. Messages may be AI or
@@ -121,6 +121,8 @@ export class EmailUsWindow extends LitElement {
121
121
  orgLegalName = "";
122
122
  @property({ attribute: false })
123
123
  country: string | null = null;
124
+ @property({ attribute: false })
125
+ buildingState: string | null = null;
124
126
 
125
127
  @property({ attribute: true })
126
128
  private leadSourceClient: LeadSourceClient | null = null;
@@ -492,6 +494,7 @@ export class EmailUsWindow extends LitElement {
492
494
  orgLegalName: this.orgLegalName,
493
495
  orgSlug: this.orgSlug,
494
496
  country: this.country,
497
+ buildingState: this.buildingState,
495
498
  clickingButtonText: "send",
496
499
  })}
497
500
  </details-window>
@@ -1,4 +1,4 @@
1
- import { html, LitElement, TemplateResult, css } from "lit";
1
+ import { html, LitElement, PropertyValues, TemplateResult, css } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
3
  import { createRef, ref, Ref } from "lit/directives/ref.js";
4
4
  import { launcherStyles } from "./launcherStyles";
@@ -65,6 +65,9 @@ export class Launcher extends LitElement {
65
65
  @property({ type: Boolean })
66
66
  isFirstMount = false;
67
67
 
68
+ @property({ attribute: false })
69
+ openSSTWindowOnLoad = false;
70
+
68
71
  @property({ attribute: false })
69
72
  autoOpenChatWidget = false;
70
73
 
@@ -78,6 +81,8 @@ export class Launcher extends LitElement {
78
81
  orgLegalName = "";
79
82
  @property({ attribute: false })
80
83
  country: string | null = null;
84
+ @property({ attribute: false })
85
+ buildingState: string | null = null;
81
86
  @property({ attribute: true })
82
87
  chatCallUsHeader = "";
83
88
  @property({ attribute: false })
@@ -283,6 +288,15 @@ export class Launcher extends LitElement {
283
288
  tourSchedulerRef: Ref<TourScheduler> = createRef();
284
289
  calculatorWindowRef: Ref<FeeCalculator> = createRef();
285
290
 
291
+ willUpdate(changedProperties: PropertyValues<this>): void {
292
+ if (
293
+ changedProperties.has("openSSTWindowOnLoad") &&
294
+ this.openSSTWindowOnLoad
295
+ ) {
296
+ this.isSSTWindowOpen = true;
297
+ }
298
+ }
299
+
286
300
  updated = async (): Promise<void> => {
287
301
  this.attachOnClickToEmailUsWindow();
288
302
  this.attachOnClickToSSTWindow();
@@ -1328,6 +1342,7 @@ export class Launcher extends LitElement {
1328
1342
  .leadSourceMultitouchClient=${this.leadSourceMultitouchClient}
1329
1343
  .orgLegalName=${this.orgLegalName}
1330
1344
  .country=${this.country}
1345
+ .buildingState=${this.buildingState}
1331
1346
  >
1332
1347
  </email-us-window>
1333
1348
  </div>`
@@ -1357,6 +1372,7 @@ export class Launcher extends LitElement {
1357
1372
  .bedroomPreferenceRequired=${this.bedroomPreferenceRequired}
1358
1373
  .orgLegalName=${this.orgLegalName}
1359
1374
  .country=${this.country}
1375
+ .buildingState=${this.buildingState}
1360
1376
  buildingId=${this.buildingId}
1361
1377
  orgId=${this.orgId}
1362
1378
  .leadSourceParams=${this.leadSourceParams}
@@ -1402,6 +1418,7 @@ export class Launcher extends LitElement {
1402
1418
  .leadSourceMultitouchClient=${this.leadSourceMultitouchClient}
1403
1419
  .orgLegalName=${this.orgLegalName}
1404
1420
  .country=${this.country}
1421
+ .buildingState=${this.buildingState}
1405
1422
  ></call-us-window>
1406
1423
  </div>
1407
1424
  `
@@ -71,6 +71,10 @@ import {
71
71
 
72
72
  import "./chat-additional-actions";
73
73
  import { LeadSourceMultitouchClient } from "./LeadSourceMultitouchClient";
74
+ import {
75
+ shouldOpenTourSchedulerFromUrl,
76
+ storeGoogleReserveReferral,
77
+ } from "../googleReserve";
74
78
 
75
79
  @customElement("me-chat")
76
80
  export class MEChat extends LitElement {
@@ -187,6 +191,9 @@ export class MEChat extends LitElement {
187
191
  @state()
188
192
  private showTourNextToChat = false;
189
193
 
194
+ @state()
195
+ private openTourSchedulerOnLoad = false;
196
+
190
197
  @state()
191
198
  private displayPubnubChat = false;
192
199
 
@@ -321,19 +328,24 @@ export class MEChat extends LitElement {
321
328
 
322
329
  this.attachOnClickToLauncher();
323
330
 
324
- if (this.shouldAutoOpenChat(this.webchatConfigHasAutoOpenChat)) {
331
+ storeGoogleReserveReferral(this.buildingWebchatView?.id ?? 0);
332
+
333
+ if (
334
+ shouldOpenTourSchedulerFromUrl() &&
335
+ (this.enabledChatWidgets.sstDesktop || this.enabledChatWidgets.sstMobile)
336
+ ) {
337
+ this.openTourSchedulerOnLoad = true;
338
+ } else if (this.shouldAutoOpenChat(this.webchatConfigHasAutoOpenChat)) {
325
339
  this.displayPubnubChat = true;
326
340
  this.hideLauncher = true;
327
341
  this.hasMounted = true;
328
342
  this.updateAlreadyAutoOpenedTimestamp();
329
- }
330
-
331
- this.isLoading = false;
332
-
333
- if (localStorage.getItem("isChatOpen") === "true") {
343
+ } else if (localStorage.getItem("isChatOpen") === "true") {
334
344
  this.hideLauncher = true;
335
345
  this.displayPubnubChat = true;
336
346
  }
347
+
348
+ this.isLoading = false;
337
349
  };
338
350
 
339
351
  // The general info for the buildingg
@@ -1032,6 +1044,7 @@ export class MEChat extends LitElement {
1032
1044
  ?.usesDynamicScheduling ?? false}
1033
1045
  .orgLegalName=${this.buildingWebchatView?.orgLegalName ?? ""}
1034
1046
  .country=${this.buildingWebchatView?.country ?? null}
1047
+ .buildingState=${this.buildingWebchatView?.state ?? null}
1035
1048
  .tourTypeOptions=${this.buildingWebchatView
1036
1049
  ?.tourTypeOptions ?? []}
1037
1050
  .layoutOptions=${this.buildingWebchatView?.layoutOptions ??
@@ -1080,6 +1093,7 @@ export class MEChat extends LitElement {
1080
1093
  virtualToursTypeOffered="${this.buildingWebchatView
1081
1094
  .virtualToursTypeOffered ?? ""}"
1082
1095
  .showTourNextToChat="${this.showTourNextToChat ?? ""}"
1096
+ .openSSTWindowOnLoad=${this.openTourSchedulerOnLoad}
1083
1097
  @closeShowTourNextToChat=${() => {
1084
1098
  this.showTourNextToChat = false;
1085
1099
  this.hideLauncher = true;
@@ -12,6 +12,9 @@ const disclaimerStyles = {
12
12
  link: {
13
13
  color: "#4287f5",
14
14
  },
15
+ paragraph: {
16
+ margin: "0 0 4px",
17
+ },
15
18
  };
16
19
 
17
20
  // States where we need to say "you are interacting with generative AI".
@@ -23,6 +26,129 @@ export const isCanadianBuilding = (
23
26
  country: string | null | undefined
24
27
  ): boolean => country === "CA";
25
28
 
29
+ const QUEBEC_PRIVACY_POLICY_URL = "https://eliseai.com/fr/canada-policy";
30
+ const QUEBEC_TERMS_URL = "https://eliseai.com/fr/msgterms";
31
+
32
+ export const isQuebecBuilding = (state: string | null | undefined): boolean =>
33
+ state?.trim().toUpperCase() === "QC";
34
+
35
+ type QuebecConsentForm = "emailUs" | "textUs" | "scheduleTour";
36
+
37
+ const disclaimerLink = (href: string, label: string): TemplateResult =>
38
+ html`<a
39
+ style=${styleMap(disclaimerStyles.link)}
40
+ href=${href}
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ >${label}</a
44
+ >`;
45
+
46
+ const quebecConsentText = (
47
+ form: QuebecConsentForm,
48
+ orgLegalName: string
49
+ ): [TemplateResult, TemplateResult] => {
50
+ const frenchLinks = html`${disclaimerLink(
51
+ QUEBEC_PRIVACY_POLICY_URL,
52
+ "Politique relative à la protection des renseignements personnels"
53
+ )}
54
+ ainsi qu’à ces ${disclaimerLink(QUEBEC_TERMS_URL, "Conditions générales")}`;
55
+ const englishLinks = html`${disclaimerLink(
56
+ QUEBEC_PRIVACY_POLICY_URL,
57
+ "privacy policy"
58
+ )}
59
+ and these ${disclaimerLink(QUEBEC_TERMS_URL, "terms and conditions")}`;
60
+
61
+ if (form === "textUs") {
62
+ return [
63
+ html`Lorsque vous fournissez votre numéro de téléphone et que vous cliquez
64
+ sur Envoyer (send), vous consentez à recevoir des messages électroniques
65
+ et des appels récurrents de nature commerciale de la part de
66
+ ${orgLegalName} ou en son nom à ce numéro, dans la mesure où cela est
67
+ permis par les lois applicables. Les messages peuvent être générés par une
68
+ intelligence artificielle (IA) ou par un humain. Frais std SMS/données
69
+ applicables. Vous consentez à cette ${frenchLinks}, y compris à ce que
70
+ notre fournisseur de services, EliseAI, enregistre et utilise votre numéro
71
+ de téléphone et vos communications afin de générer des réponses et
72
+ d’améliorer ses services. Vous pouvez retirer votre consentement en tout
73
+ temps en vous désabonnant ou en communiquant avec nous.`,
74
+ html`By providing your number and clicking send, you consent to receive
75
+ recurring commercial electronic messages and calls from or on behalf of
76
+ ${orgLegalName} at this number, to the extent permitted by applicable law.
77
+ Messages may be AI or human generated. StdMsg&DataRatesMayApply. You
78
+ consent to this ${englishLinks}, including having your number and
79
+ communications recorded and used by our service provider EliseAI to
80
+ generate responses and improve their services. You may withdraw your
81
+ consent at any time by unsubscribing or contacting us.`,
82
+ ];
83
+ }
84
+
85
+ if (form === "scheduleTour") {
86
+ return [
87
+ html`Lorsque vous fournissez votre numéro de téléphone ou votre adresse
88
+ courriel et que vous cliquez sur Planifier une visite (schedule tour),
89
+ vous consentez à recevoir des messages électroniques et des appels
90
+ récurrents de nature commerciale de la part de ${orgLegalName} ou en son
91
+ nom à ce numéro ou à cette adresse courriel, dans la mesure où cela est
92
+ permis par les lois applicables. Les messages peuvent être générés par une
93
+ intelligence artificielle (IA) ou par un humain. Frais std SMS/données
94
+ applicables. Vous consentez à cette ${frenchLinks}, y compris à ce que
95
+ notre fournisseur de services, EliseAI, enregistre et utilise votre numéro
96
+ de téléphone, votre adresse courriel et vos communications afin de générer
97
+ des réponses et d’améliorer ses services. Vous pouvez retirer votre
98
+ consentement en tout temps en vous désabonnant ou en communiquant avec
99
+ nous.`,
100
+ html`By providing your number and/or email address and clicking schedule
101
+ tour, you consent to receive recurring commercial electronic messages and
102
+ calls from or on behalf of ${orgLegalName} at the provided number and/or
103
+ email address, to the extent permitted by applicable law. Messages may be
104
+ AI or human generated. StdMsg&DataRatesMayApply. You consent to this
105
+ ${englishLinks}, including having your number, email address, and
106
+ communications recorded and used by our service provider EliseAI to
107
+ generate responses and improve their services. You may withdraw your
108
+ consent at any time by unsubscribing or contacting us.`,
109
+ ];
110
+ }
111
+
112
+ return [
113
+ html`Lorsque vous fournissez votre adresse courriel ou votre numéro de
114
+ téléphone et que vous cliquez sur Envoyer (send), vous consentez à recevoir
115
+ des messages électroniques et des appels récurrents de nature commerciale de
116
+ la part de ${orgLegalName} ou en son nom à cette adresse courriel ou à ce
117
+ numéro, dans la mesure où cela est permis par les lois applicables. Les
118
+ messages peuvent être générés par une intelligence artificielle (IA) ou par
119
+ un humain. Vous consentez à cette ${frenchLinks}, y compris à ce que notre
120
+ fournisseur de services, EliseAI, enregistre et utilise votre adresse
121
+ courriel, votre numéro de téléphone et vos communications afin de générer
122
+ des réponses et d’améliorer ses services. Vous pouvez retirer votre
123
+ consentement en tout temps en vous désabonnant ou en communiquant avec nous.`,
124
+ html`By providing your email address and/or number and clicking send, you
125
+ consent to receive recurring commercial electronic messages and calls from
126
+ or on behalf of ${orgLegalName} at that email address and/or number, to the
127
+ extent permitted by applicable law. Messages may be AI or human generated.
128
+ You consent to this ${englishLinks}, including having your email address,
129
+ number, and communications recorded and used by our service provider EliseAI
130
+ to generate responses and improve their services. You may withdraw your
131
+ consent at any time by unsubscribing or contacting us.`,
132
+ ];
133
+ };
134
+
135
+ export const quebecConsentDisclaimer = (
136
+ form: QuebecConsentForm,
137
+ orgLegalName: string,
138
+ hasPhoneNumber = false
139
+ ): TemplateResult => {
140
+ const [french, english] = quebecConsentText(form, orgLegalName);
141
+ const paragraphStyle = styleMap(disclaimerStyles.paragraph);
142
+ return html`<p style=${paragraphStyle}>${french}</p>
143
+ <p style=${paragraphStyle}>${english}</p>
144
+ ${form === "textUs" || hasPhoneNumber
145
+ ? html`<p style=${paragraphStyle}>
146
+ Msg freq varies. Text STOP to opt-out of messages. Text HELP for
147
+ assistance.
148
+ </p>`
149
+ : ""}`;
150
+ };
151
+
26
152
  export const CURRENT_CONSENT_ID = "currentConsentId";
27
153
 
28
154
  const formDisclaimer = ({
@@ -31,6 +157,7 @@ const formDisclaimer = ({
31
157
  orgLegalName,
32
158
  orgSlug,
33
159
  country,
160
+ buildingState,
34
161
  clickingButtonText = "send",
35
162
  }: {
36
163
  buildingName: string;
@@ -39,8 +166,19 @@ const formDisclaimer = ({
39
166
  orgLegalName: string;
40
167
  orgSlug: string;
41
168
  country?: string | null;
169
+ buildingState?: string | null;
42
170
  clickingButtonText?: string;
43
171
  }): TemplateResult => {
172
+ if (isQuebecBuilding(buildingState)) {
173
+ return html`<div style=${styleMap(disclaimerStyles.container)}>
174
+ ${quebecConsentDisclaimer(
175
+ clickingButtonText === "schedule tour" ? "scheduleTour" : "emailUs",
176
+ orgLegalName,
177
+ !!phoneNumberInput
178
+ )}
179
+ </div>`;
180
+ }
181
+
44
182
  const policyUrl = isCanadianBuilding(country)
45
183
  ? CANADA_PRIVACY_POLICY_URL
46
184
  : "http://bit.ly/me_privacy_policy";
@@ -0,0 +1,60 @@
1
+ import { expect } from "@esm-bundle/chai";
2
+ import {
3
+ readGoogleReserveReferral,
4
+ shouldOpenTourSchedulerFromUrl,
5
+ storeGoogleReserveReferral,
6
+ } from "./googleReserve";
7
+
8
+ const STORAGE_KEY = "com.eliseai.webchat.googlereserve.referral";
9
+
10
+ const visit = (search: string): void =>
11
+ history.replaceState(null, "", `${location.pathname}${search}`);
12
+
13
+ describe("Google Reserve", () => {
14
+ const originalSearch = location.search;
15
+
16
+ beforeEach(() => {
17
+ localStorage.removeItem(STORAGE_KEY);
18
+ });
19
+
20
+ afterEach(() => {
21
+ localStorage.removeItem(STORAGE_KEY);
22
+ visit(originalSearch);
23
+ });
24
+
25
+ it("remembers a click from a Google listing, keeping the token intact", () => {
26
+ visit("?elise_open=tour&rwg_token=AFd1x%3D%3D");
27
+ storeGoogleReserveReferral(7);
28
+ visit("");
29
+
30
+ expect(readGoogleReserveReferral()).to.include({
31
+ rwgToken: "AFd1x==",
32
+ buildingId: 7,
33
+ });
34
+ });
35
+
36
+ it("has no referral without a token or once it has expired", () => {
37
+ visit("?utm_source=website");
38
+ storeGoogleReserveReferral(7);
39
+ expect(readGoogleReserveReferral()).to.equal(null);
40
+
41
+ localStorage.setItem(
42
+ STORAGE_KEY,
43
+ JSON.stringify({
44
+ rwgToken: "AFd1x==",
45
+ buildingId: 7,
46
+ expiresAt: Date.now() - 1,
47
+ })
48
+ );
49
+ expect(readGoogleReserveReferral()).to.equal(null);
50
+ });
51
+
52
+ it("opens the tour scheduler only when the URL asks for it", () => {
53
+ visit("?elise_open=tour");
54
+ expect(shouldOpenTourSchedulerFromUrl()).to.equal(true);
55
+ visit("?elise_open=chat");
56
+ expect(shouldOpenTourSchedulerFromUrl()).to.equal(false);
57
+ visit("");
58
+ expect(shouldOpenTourSchedulerFromUrl()).to.equal(false);
59
+ });
60
+ });
@@ -0,0 +1,48 @@
1
+ // Google requires the referral token kept for 30 days after the click.
2
+ const REFERRAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
3
+ const REFERRAL_STORAGE_KEY = "com.eliseai.webchat.googlereserve.referral";
4
+ // Google "Book" links to a customer's page add this so the widget opens the tour scheduler.
5
+ const OPEN_PARAM = "elise_open";
6
+
7
+ type GoogleReserveReferral = {
8
+ rwgToken: string;
9
+ buildingId: number;
10
+ expiresAt: number;
11
+ };
12
+
13
+ export const shouldOpenTourSchedulerFromUrl = (): boolean =>
14
+ new URLSearchParams(window.location.search).get(OPEN_PARAM) === "tour";
15
+
16
+ /**
17
+ * Remembers a click from a building's Google listing, which arrives with an
18
+ * `rwg_token` query param, so a booking within 30 days can be reported back.
19
+ */
20
+ export const storeGoogleReserveReferral = (buildingId: number): void => {
21
+ const rwgToken = new URLSearchParams(window.location.search).get("rwg_token");
22
+ if (!rwgToken || !buildingId) {
23
+ return;
24
+ }
25
+ const referral: GoogleReserveReferral = {
26
+ rwgToken,
27
+ buildingId,
28
+ expiresAt: Date.now() + REFERRAL_TTL_MS,
29
+ };
30
+ localStorage.setItem(REFERRAL_STORAGE_KEY, JSON.stringify(referral));
31
+ };
32
+
33
+ /**
34
+ * The visitor's click from a building's Google listing within the last 30 days,
35
+ * sent with a booking so the backend can report it to Google.
36
+ */
37
+ export const readGoogleReserveReferral = (): GoogleReserveReferral | null => {
38
+ try {
39
+ const referral: GoogleReserveReferral | null = JSON.parse(
40
+ localStorage.getItem(REFERRAL_STORAGE_KEY) ?? "null"
41
+ );
42
+ return referral?.rwgToken && Date.now() < referral.expiresAt
43
+ ? referral
44
+ : null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ };
@@ -0,0 +1,161 @@
1
+ import { expect } from "@esm-bundle/chai";
2
+ import { restore, useFakeXMLHttpRequest } from "sinon/pkg/sinon-esm";
3
+ import "../public/dist/index";
4
+
5
+ type Region = { state: string | null; country: string | null };
6
+ type Widget = HTMLElement & { updateComplete: Promise<boolean> };
7
+ type WidgetCase = {
8
+ name: string;
9
+ tag: string;
10
+ props: Record<string, string>;
11
+ propsAfterMount?: Record<string, boolean>;
12
+ french: string;
13
+ english: string;
14
+ ontarioCopy: string;
15
+ newYorkCopy: string;
16
+ newYorkLinks: string[];
17
+ };
18
+
19
+ const ORG = "Test Org Legal Inc.";
20
+ const QUEBEC: Region[] = [
21
+ { state: "QC", country: "CA" },
22
+ { state: "qc", country: null },
23
+ ];
24
+ const ONTARIO: Region = { state: "ON", country: "CA" };
25
+ const NEW_YORK: Region = { state: "NY", country: "US" };
26
+
27
+ const FR_PRIVACY =
28
+ "Politique relative à la protection des renseignements personnels";
29
+ const PHONE_OPT_OUT =
30
+ "Msg freq varies. Text STOP to opt-out of messages. Text HELP for assistance.";
31
+
32
+ const EMAIL_US_FR = `Lorsque vous fournissez votre adresse courriel ou votre numéro de téléphone et que vous cliquez sur Envoyer (send), vous consentez à recevoir des messages électroniques et des appels récurrents de nature commerciale de la part de ${ORG} ou en son nom à cette adresse courriel ou à ce numéro, dans la mesure où cela est permis par les lois applicables. Les messages peuvent être générés par une intelligence artificielle (IA) ou par un humain. Vous consentez à cette ${FR_PRIVACY} ainsi qu’à ces Conditions générales, y compris à ce que notre fournisseur de services, EliseAI, enregistre et utilise votre adresse courriel, votre numéro de téléphone et vos communications afin de générer des réponses et d’améliorer ses services. Vous pouvez retirer votre consentement en tout temps en vous désabonnant ou en communiquant avec nous.`;
33
+ const EMAIL_US_EN = `By providing your email address and/or number and clicking send, you consent to receive recurring commercial electronic messages and calls from or on behalf of ${ORG} at that email address and/or number, to the extent permitted by applicable law. Messages may be AI or human generated. You consent to this privacy policy and these terms and conditions, including having your email address, number, and communications recorded and used by our service provider EliseAI to generate responses and improve their services. You may withdraw your consent at any time by unsubscribing or contacting us.`;
34
+
35
+ const TEXT_US_FR = `Lorsque vous fournissez votre numéro de téléphone et que vous cliquez sur Envoyer (send), vous consentez à recevoir des messages électroniques et des appels récurrents de nature commerciale de la part de ${ORG} ou en son nom à ce numéro, dans la mesure où cela est permis par les lois applicables. Les messages peuvent être générés par une intelligence artificielle (IA) ou par un humain. Frais std SMS/données applicables. Vous consentez à cette ${FR_PRIVACY} ainsi qu’à ces Conditions générales, y compris à ce que notre fournisseur de services, EliseAI, enregistre et utilise votre numéro de téléphone et vos communications afin de générer des réponses et d’améliorer ses services. Vous pouvez retirer votre consentement en tout temps en vous désabonnant ou en communiquant avec nous.`;
36
+ const TEXT_US_EN = `By providing your number and clicking send, you consent to receive recurring commercial electronic messages and calls from or on behalf of ${ORG} at this number, to the extent permitted by applicable law. Messages may be AI or human generated. StdMsg&DataRatesMayApply. You consent to this privacy policy and these terms and conditions, including having your number and communications recorded and used by our service provider EliseAI to generate responses and improve their services. You may withdraw your consent at any time by unsubscribing or contacting us.`;
37
+
38
+ const SCHEDULE_TOUR_FR = `Lorsque vous fournissez votre numéro de téléphone ou votre adresse courriel et que vous cliquez sur Planifier une visite (schedule tour), vous consentez à recevoir des messages électroniques et des appels récurrents de nature commerciale de la part de ${ORG} ou en son nom à ce numéro ou à cette adresse courriel, dans la mesure où cela est permis par les lois applicables. Les messages peuvent être générés par une intelligence artificielle (IA) ou par un humain. Frais std SMS/données applicables. Vous consentez à cette ${FR_PRIVACY} ainsi qu’à ces Conditions générales, y compris à ce que notre fournisseur de services, EliseAI, enregistre et utilise votre numéro de téléphone, votre adresse courriel et vos communications afin de générer des réponses et d’améliorer ses services. Vous pouvez retirer votre consentement en tout temps en vous désabonnant ou en communiquant avec nous.`;
39
+ const SCHEDULE_TOUR_EN = `By providing your number and/or email address and clicking schedule tour, you consent to receive recurring commercial electronic messages and calls from or on behalf of ${ORG} at the provided number and/or email address, to the extent permitted by applicable law. Messages may be AI or human generated. StdMsg&DataRatesMayApply. You consent to this privacy policy and these terms and conditions, including having your number, email address, and communications recorded and used by our service provider EliseAI to generate responses and improve their services. You may withdraw your consent at any time by unsubscribing or contacting us.`;
40
+
41
+ const renderedText = (root: ShadowRoot | null | undefined): string =>
42
+ (root?.textContent ?? "").replace(/\s+/g, " ");
43
+
44
+ const linkHrefs = (root: ShadowRoot | null | undefined): string[] =>
45
+ Array.from(root?.querySelectorAll("a") ?? []).map(
46
+ (a) => `${a.textContent?.trim()} -> ${a.getAttribute("href")}`
47
+ );
48
+
49
+ const expectQuebecConsent = (
50
+ root: ShadowRoot | null | undefined,
51
+ french: string,
52
+ english: string
53
+ ): void => {
54
+ const text = renderedText(root);
55
+ expect(text).to.include(french);
56
+ expect(text).to.include(english);
57
+ expect(text.indexOf(french)).to.be.lessThan(text.indexOf(english));
58
+ expect(text.indexOf(english)).to.be.lessThan(text.indexOf(PHONE_OPT_OUT));
59
+ expect(linkHrefs(root)).to.include.members([
60
+ `${FR_PRIVACY} -> https://eliseai.com/fr/canada-policy`,
61
+ "Conditions générales -> https://eliseai.com/fr/msgterms",
62
+ "privacy policy -> https://eliseai.com/fr/canada-policy",
63
+ "terms and conditions -> https://eliseai.com/fr/msgterms",
64
+ ]);
65
+ };
66
+
67
+ const expectNoFrench = (root: ShadowRoot | null | undefined): void => {
68
+ expect(renderedText(root)).not.to.include("Lorsque vous fournissez");
69
+ };
70
+
71
+ const US_PRIVACY_LINK = "privacy policy -> http://bit.ly/me_privacy_policy";
72
+ const PHONE = "(514) 555-0123";
73
+
74
+ const WIDGETS: WidgetCase[] = [
75
+ {
76
+ name: "Email Us form",
77
+ tag: "email-us-window",
78
+ props: { email: "local.test@example.com", phoneNumber: PHONE },
79
+ french: EMAIL_US_FR,
80
+ english: EMAIL_US_EN,
81
+ ontarioCopy: `By providing your email address and/or number and clicking send, you consent to receive recurring commercial electronic messages and calls from or on behalf of ${ORG} at that email address and/or number. Messages may be AI or human generated.`,
82
+ newYorkCopy: `By providing your number and clicking send, you consent to receive recurring marketing text and voice messages and calls from or on behalf of ${ORG} at this number using artificial voice or an autodialer system.`,
83
+ newYorkLinks: [US_PRIVACY_LINK],
84
+ },
85
+ {
86
+ name: "Text Us form",
87
+ tag: "call-us-window",
88
+ props: { hasTextUsEnabled: "true" },
89
+ french: TEXT_US_FR,
90
+ english: TEXT_US_EN,
91
+ ontarioCopy: `By providing your number and clicking send, you consent to receive recurring commercial electronic messages from or on behalf of ${ORG} at this number.`,
92
+ newYorkCopy: `By providing your number and clicking send, you consent to receive recurring marketing text and voice messages and calls from or on behalf of ${ORG} at this number using artificial voice or an autodialer system.`,
93
+ newYorkLinks: [
94
+ US_PRIVACY_LINK,
95
+ "terms and conditions -> https://eliseai.com/msgterms",
96
+ ],
97
+ },
98
+ {
99
+ name: "tour scheduler",
100
+ tag: "tour-scheduler",
101
+ props: { phoneNumber: PHONE },
102
+ propsAfterMount: {
103
+ waitingForAvailabilities: false,
104
+ shouldAllowScheduleLoading: false,
105
+ shouldAllowScheduling: true,
106
+ },
107
+ french: SCHEDULE_TOUR_FR,
108
+ english: SCHEDULE_TOUR_EN,
109
+ ontarioCopy: `By providing your number and/or email address and clicking schedule tour, you consent to receive recurring commercial electronic messages and calls from or on behalf of ${ORG} at the provided number and/or email address. Messages may be AI or human generated.`,
110
+ newYorkCopy: `By providing your number and clicking schedule tour, you consent to receive recurring marketing text and voice messages and calls from or on behalf of ${ORG} at this number using artificial voice or an autodialer system.`,
111
+ newYorkLinks: [US_PRIVACY_LINK],
112
+ },
113
+ ];
114
+
115
+ const renderWidget = async (
116
+ widget: WidgetCase,
117
+ region: Region
118
+ ): Promise<Widget> => {
119
+ const element = document.createElement(widget.tag) as Widget;
120
+ Object.assign(element, widget.props, {
121
+ orgLegalName: ORG,
122
+ country: region.country,
123
+ buildingState: region.state,
124
+ });
125
+ document.body.appendChild(element);
126
+ Object.assign(element, widget.propsAfterMount);
127
+ await element.updateComplete;
128
+ return element;
129
+ };
130
+
131
+ afterEach(() => {
132
+ restore();
133
+ });
134
+
135
+ for (const widget of WIDGETS) {
136
+ it(`shows legal's French then English consent on the ${widget.name} only for Quebec`, async () => {
137
+ useFakeXMLHttpRequest();
138
+
139
+ for (const region of QUEBEC) {
140
+ const quebec = await renderWidget(widget, region);
141
+ expectQuebecConsent(quebec.shadowRoot, widget.french, widget.english);
142
+ quebec.remove();
143
+ }
144
+
145
+ const ontario = await renderWidget(widget, ONTARIO);
146
+ expectNoFrench(ontario.shadowRoot);
147
+ expect(renderedText(ontario.shadowRoot)).to.include(widget.ontarioCopy);
148
+ expect(linkHrefs(ontario.shadowRoot)).to.include(
149
+ "privacy policy -> https://eliseai.com/canada-policy"
150
+ );
151
+ ontario.remove();
152
+
153
+ const newYork = await renderWidget(widget, NEW_YORK);
154
+ expectNoFrench(newYork.shadowRoot);
155
+ expect(renderedText(newYork.shadowRoot)).to.include(widget.newYorkCopy);
156
+ expect(linkHrefs(newYork.shadowRoot)).to.include.members(
157
+ widget.newYorkLinks
158
+ );
159
+ newYork.remove();
160
+ });
161
+ }