@meetelise/chat 1.54.1 → 1.55.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/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +21 -0
- package/dist/src/WebComponent/actions/email-us-window.d.ts +1 -7
- package/dist/src/WebComponent/brokerReferral.d.ts +17 -0
- package/package.json +1 -1
- package/public/dist/index.js +550 -357
- package/src/WebComponent/Scheduler/tour-scheduler.ts +253 -32
- package/src/WebComponent/Scheduler/tourSchedulerStyles.ts +67 -1
- package/src/WebComponent/actions/email-us-window.ts +72 -51
- package/src/WebComponent/brokerReferral.ts +55 -0
- package/src/WebComponent/launcher/Launcher.ts +1 -0
- package/src/brokerReferralSubmission.test.ts +149 -0
|
@@ -21,6 +21,13 @@ import {
|
|
|
21
21
|
LeadSourceMultitouchClient,
|
|
22
22
|
LeadSourceSubmittedListItem,
|
|
23
23
|
} from "../LeadSourceMultitouchClient";
|
|
24
|
+
import type { BrokerReferral } from "../brokerReferral";
|
|
25
|
+
import {
|
|
26
|
+
brokerEmailError,
|
|
27
|
+
brokerNameIsMissing,
|
|
28
|
+
brokerRequestFields,
|
|
29
|
+
toBrokerReferral,
|
|
30
|
+
} from "../brokerReferral";
|
|
24
31
|
|
|
25
32
|
@customElement("email-us-window")
|
|
26
33
|
export class EmailUsWindow extends LitElement {
|
|
@@ -62,11 +69,6 @@ export class EmailUsWindow extends LitElement {
|
|
|
62
69
|
gap: 8px;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
/* With the broker toggle on its left, the toggle and Send sit at either end. */
|
|
66
|
-
.action-bttn-container--with-toggle {
|
|
67
|
-
justify-content: space-between;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
72
|
.email-us__message-input {
|
|
71
73
|
resize: none;
|
|
72
74
|
width: 100%;
|
|
@@ -99,14 +101,58 @@ export class EmailUsWindow extends LitElement {
|
|
|
99
101
|
color: rgb(235, 64, 52);
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
/* The question lines up with the fields' text, its switch with their right edge. */
|
|
102
105
|
.email-us__broker-toggle {
|
|
103
106
|
display: flex;
|
|
104
107
|
align-items: center;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
gap: 12px;
|
|
110
|
+
min-height: 32px;
|
|
111
|
+
padding: 0 17px;
|
|
112
|
+
font-family: "Helvetica Neue", Arial;
|
|
113
|
+
font-size: 14px;
|
|
114
|
+
color: #202020;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.broker-switch {
|
|
119
|
+
appearance: none;
|
|
120
|
+
-webkit-appearance: none;
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
position: relative;
|
|
123
|
+
width: 40px;
|
|
124
|
+
height: 24px;
|
|
125
|
+
margin: 0;
|
|
126
|
+
border-radius: 100px;
|
|
127
|
+
background: #bdbdbd;
|
|
109
128
|
cursor: pointer;
|
|
129
|
+
transition: background 0.15s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.broker-switch::before {
|
|
133
|
+
content: "";
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: 2px;
|
|
136
|
+
left: 2px;
|
|
137
|
+
width: 20px;
|
|
138
|
+
height: 20px;
|
|
139
|
+
border-radius: 50%;
|
|
140
|
+
background: #ffffff;
|
|
141
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
142
|
+
transition: transform 0.15s;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.broker-switch:checked {
|
|
146
|
+
background: #202020;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.broker-switch:checked::before {
|
|
150
|
+
transform: translateX(16px);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.broker-switch:focus-visible {
|
|
154
|
+
outline: 2px solid #202020;
|
|
155
|
+
outline-offset: 2px;
|
|
110
156
|
}
|
|
111
157
|
|
|
112
158
|
.email-us__section-label {
|
|
@@ -331,12 +377,12 @@ export class EmailUsWindow extends LitElement {
|
|
|
331
377
|
/** The broker's details to send, only when the submitter says they're referring a client. */
|
|
332
378
|
brokerReferral = (): BrokerReferral | null =>
|
|
333
379
|
this.brokerIntakeEnabled && this.isBrokerReferral
|
|
334
|
-
?
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
380
|
+
? toBrokerReferral(
|
|
381
|
+
this.brokerFirstName,
|
|
382
|
+
this.brokerLastName,
|
|
383
|
+
this.brokerEmail,
|
|
384
|
+
this.brokerFirmName
|
|
385
|
+
)
|
|
340
386
|
: null;
|
|
341
387
|
|
|
342
388
|
validateFormFields = (): void => {
|
|
@@ -349,17 +395,8 @@ export class EmailUsWindow extends LitElement {
|
|
|
349
395
|
this.brokerEmailError = null;
|
|
350
396
|
const broker = this.brokerReferral();
|
|
351
397
|
if (broker) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
// Broker intake opens a broker thread only for a broker email, and a broker can't be their own client.
|
|
356
|
-
if (!broker.email || !isValidEmail(broker.email)) {
|
|
357
|
-
this.brokerEmailError = "Enter your valid email address";
|
|
358
|
-
} else if (
|
|
359
|
-
broker.email.toLowerCase() === this.email.trim().toLowerCase()
|
|
360
|
-
) {
|
|
361
|
-
this.brokerEmailError = "Enter your client's details below, not yours";
|
|
362
|
-
}
|
|
398
|
+
this.hasBrokerNameError = brokerNameIsMissing(broker);
|
|
399
|
+
this.brokerEmailError = brokerEmailError(broker, this.email);
|
|
363
400
|
}
|
|
364
401
|
if (!this.firstName || !this.lastName) {
|
|
365
402
|
this.hasNameError = true;
|
|
@@ -464,13 +501,15 @@ export class EmailUsWindow extends LitElement {
|
|
|
464
501
|
|
|
465
502
|
renderBrokerToggle = (): TemplateResult => html`
|
|
466
503
|
<label class="email-us__broker-toggle">
|
|
504
|
+
<span>Are you an agent or locator?</span>
|
|
467
505
|
<input
|
|
468
506
|
type="checkbox"
|
|
507
|
+
role="switch"
|
|
469
508
|
id="broker-referral-toggle"
|
|
509
|
+
class="broker-switch"
|
|
470
510
|
.checked=${this.isBrokerReferral}
|
|
471
511
|
@change=${this.onToggleBrokerReferral}
|
|
472
512
|
/>
|
|
473
|
-
Agent or broker? Refer a client
|
|
474
513
|
</label>
|
|
475
514
|
`;
|
|
476
515
|
|
|
@@ -667,6 +706,10 @@ export class EmailUsWindow extends LitElement {
|
|
|
667
706
|
`
|
|
668
707
|
: ""}
|
|
669
708
|
<div class="email-us__vertical-spacer"></div>
|
|
709
|
+
${this.brokerIntakeEnabled
|
|
710
|
+
? html`${this.renderBrokerToggle()}
|
|
711
|
+
<div class="email-us__vertical-spacer"></div>`
|
|
712
|
+
: ""}
|
|
670
713
|
${this.leadSources.length > 0 &&
|
|
671
714
|
(this.featureFlagShowDropdown === FeatureFlagsShowDropdown.always ||
|
|
672
715
|
(this.featureFlagShowDropdown ===
|
|
@@ -705,13 +748,7 @@ export class EmailUsWindow extends LitElement {
|
|
|
705
748
|
${this.hasMessageError
|
|
706
749
|
? html` <div class="email-us__error-text">Include a message</div> `
|
|
707
750
|
: ""}
|
|
708
|
-
<div
|
|
709
|
-
class=${classMap({
|
|
710
|
-
["action-bttn-container"]: true,
|
|
711
|
-
["action-bttn-container--with-toggle"]: this.brokerIntakeEnabled,
|
|
712
|
-
})}
|
|
713
|
-
>
|
|
714
|
-
${this.brokerIntakeEnabled ? this.renderBrokerToggle() : ""}
|
|
751
|
+
<div class="action-bttn-container">
|
|
715
752
|
<action-confirm-button
|
|
716
753
|
.onClick=${this.onClick}
|
|
717
754
|
.isLoading=${this.isSubmitting}
|
|
@@ -746,14 +783,6 @@ export const installEmailUsWindow = (): void => {
|
|
|
746
783
|
}
|
|
747
784
|
};
|
|
748
785
|
|
|
749
|
-
/** A broker submitting the form for a client: sent as the request's broker fields. */
|
|
750
|
-
export type BrokerReferral = {
|
|
751
|
-
firstName: string;
|
|
752
|
-
lastName: string;
|
|
753
|
-
email: string;
|
|
754
|
-
firmName: string;
|
|
755
|
-
};
|
|
756
|
-
|
|
757
786
|
const createEmail = async (
|
|
758
787
|
firstName: string,
|
|
759
788
|
lastName: string,
|
|
@@ -785,15 +814,7 @@ const createEmail = async (
|
|
|
785
814
|
lead_sources_with_timestamps: (leadSourcesWithTimestamps ?? []).map(
|
|
786
815
|
(source) => snakify({ ...source })
|
|
787
816
|
),
|
|
788
|
-
|
|
789
|
-
...(broker
|
|
790
|
-
? {
|
|
791
|
-
broker_first_name: broker.firstName,
|
|
792
|
-
broker_last_name: broker.lastName,
|
|
793
|
-
broker_email: broker.email,
|
|
794
|
-
broker_firm_name: broker.firmName || null,
|
|
795
|
-
}
|
|
796
|
-
: {}),
|
|
817
|
+
...brokerRequestFields(broker),
|
|
797
818
|
};
|
|
798
819
|
pushGtmEvent("emailUsSubmitted", {
|
|
799
820
|
firstName,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isValidEmail } from "../utils";
|
|
2
|
+
|
|
3
|
+
/** A broker submitting a form for a client: sent as the request's broker fields. */
|
|
4
|
+
export type BrokerReferral = {
|
|
5
|
+
firstName: string;
|
|
6
|
+
lastName: string;
|
|
7
|
+
email: string;
|
|
8
|
+
firmName: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** The broker's typed details, trimmed, as a referral. */
|
|
12
|
+
export const toBrokerReferral = (
|
|
13
|
+
firstName: string,
|
|
14
|
+
lastName: string,
|
|
15
|
+
email: string,
|
|
16
|
+
firmName: string
|
|
17
|
+
): BrokerReferral => ({
|
|
18
|
+
firstName: firstName.trim(),
|
|
19
|
+
lastName: lastName.trim(),
|
|
20
|
+
email: email.trim(),
|
|
21
|
+
firmName: firmName.trim(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const brokerNameIsMissing = (broker: BrokerReferral): boolean =>
|
|
25
|
+
!broker.firstName || !broker.lastName;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What's wrong with the broker's email, or null. Broker intake opens a broker thread only for a
|
|
29
|
+
* broker email, and a broker can't be their own client.
|
|
30
|
+
*/
|
|
31
|
+
export const brokerEmailError = (
|
|
32
|
+
broker: BrokerReferral,
|
|
33
|
+
clientEmail: string
|
|
34
|
+
): string | null => {
|
|
35
|
+
if (!broker.email || !isValidEmail(broker.email)) {
|
|
36
|
+
return "Enter your valid email address";
|
|
37
|
+
}
|
|
38
|
+
if (broker.email.toLowerCase() === clientEmail.trim().toLowerCase()) {
|
|
39
|
+
return "Enter your client's details below, not yours";
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** The request's broker fields. The prospect fields stay the client's; broker intake links the broker to them. */
|
|
45
|
+
export const brokerRequestFields = (
|
|
46
|
+
broker: BrokerReferral | null | undefined
|
|
47
|
+
): Record<string, string | null> =>
|
|
48
|
+
broker
|
|
49
|
+
? {
|
|
50
|
+
broker_first_name: broker.firstName,
|
|
51
|
+
broker_last_name: broker.lastName,
|
|
52
|
+
broker_email: broker.email,
|
|
53
|
+
broker_firm_name: broker.firmName || null,
|
|
54
|
+
}
|
|
55
|
+
: {};
|
|
@@ -1373,6 +1373,7 @@ export class Launcher extends LitElement {
|
|
|
1373
1373
|
.tourTypeOptions=${this.tourTypeOptions}
|
|
1374
1374
|
.layoutOptions=${this.layoutOptions}
|
|
1375
1375
|
.bedroomPreferenceRequired=${this.bedroomPreferenceRequired}
|
|
1376
|
+
.brokerIntakeEnabled=${this.brokerIntakeEnabled}
|
|
1376
1377
|
.orgLegalName=${this.orgLegalName}
|
|
1377
1378
|
.country=${this.country}
|
|
1378
1379
|
.buildingState=${this.buildingState}
|
|
@@ -2,6 +2,10 @@ import { expect } from "@esm-bundle/chai";
|
|
|
2
2
|
import { restore, useFakeXMLHttpRequest } from "sinon/pkg/sinon-esm";
|
|
3
3
|
import "../public/dist/index";
|
|
4
4
|
import type { EmailUsWindow } from "./WebComponent/actions/email-us-window";
|
|
5
|
+
import type {
|
|
6
|
+
TourScheduler,
|
|
7
|
+
TourType,
|
|
8
|
+
} from "./WebComponent/Scheduler/tour-scheduler";
|
|
5
9
|
|
|
6
10
|
type FakeRequest = XMLHttpRequest & {
|
|
7
11
|
requestBody?: string;
|
|
@@ -285,3 +289,148 @@ it("keeps a filled client when the referral box is unticked and ticked again", a
|
|
|
285
289
|
]);
|
|
286
290
|
emailUs.remove();
|
|
287
291
|
});
|
|
292
|
+
|
|
293
|
+
// The tour form: same broker rules, sent with scheduleMe.
|
|
294
|
+
|
|
295
|
+
type TourHarness = {
|
|
296
|
+
tourType: TourType;
|
|
297
|
+
selectedDate: Date;
|
|
298
|
+
selectedTime: { datetime: string; offset: string };
|
|
299
|
+
phoneNumber: string;
|
|
300
|
+
phoneNumberE164: string | null;
|
|
301
|
+
email: string;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const createTour = (fields: Record<string, unknown>): TourScheduler => {
|
|
305
|
+
const tour = document.createElement("tour-scheduler") as TourScheduler;
|
|
306
|
+
Object.assign(tour as unknown as TourHarness, {
|
|
307
|
+
tourType: "guided" as TourType,
|
|
308
|
+
selectedDate: new Date("2030-01-15T00:00:00Z"),
|
|
309
|
+
selectedTime: { datetime: "2030-01-15T10:00:00", offset: "-06:00" },
|
|
310
|
+
phoneNumber: clientFields.phoneNumber,
|
|
311
|
+
phoneNumberE164: clientFields.phoneNumberE164,
|
|
312
|
+
email: clientFields.email,
|
|
313
|
+
});
|
|
314
|
+
Object.assign(tour, {
|
|
315
|
+
buildingId: 445038,
|
|
316
|
+
buildingSlug: "test-building",
|
|
317
|
+
orgSlug: "test-org",
|
|
318
|
+
firstNameInputValue: clientFields.firstName,
|
|
319
|
+
lastNameInputValue: clientFields.lastName,
|
|
320
|
+
...fields,
|
|
321
|
+
});
|
|
322
|
+
return tour;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
it("books a broker's tour with the client as the prospect and the broker fields", async () => {
|
|
326
|
+
const requests = captureRequests();
|
|
327
|
+
const tour = createTour({
|
|
328
|
+
brokerIntakeEnabled: true,
|
|
329
|
+
isBrokerReferral: true,
|
|
330
|
+
...brokerFields,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const submission = tour.submit();
|
|
334
|
+
|
|
335
|
+
expect(requests).to.have.length(1);
|
|
336
|
+
expect(requests[0].url).to.equal(
|
|
337
|
+
"https://app.meetelise.com/platformApi/state/create/v2/scheduleMe"
|
|
338
|
+
);
|
|
339
|
+
expect(JSON.parse(requests[0].requestBody ?? "{}")).to.include({
|
|
340
|
+
first_name: "Cora",
|
|
341
|
+
last_name: "Client",
|
|
342
|
+
email_address: "cora@example.com",
|
|
343
|
+
phone_number: "+13125550100",
|
|
344
|
+
broker_first_name: "Bea",
|
|
345
|
+
broker_last_name: "Broker",
|
|
346
|
+
broker_email: "bea@realty.example.com",
|
|
347
|
+
broker_firm_name: "Demo Realty",
|
|
348
|
+
});
|
|
349
|
+
requests[0].respond(200, { "Content-Type": "application/json" }, "{}");
|
|
350
|
+
await submission;
|
|
351
|
+
|
|
352
|
+
const event = window.dataLayer[window.dataLayer.length - 1];
|
|
353
|
+
expect(event).to.include({
|
|
354
|
+
event: "scheduleTourSubmitted",
|
|
355
|
+
isBrokerReferral: true,
|
|
356
|
+
});
|
|
357
|
+
expect(JSON.stringify(event)).not.to.contain("bea@realty.example.com");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("books a tour with no broker fields when broker intake is off, whatever the form holds", async () => {
|
|
361
|
+
const requests = captureRequests();
|
|
362
|
+
const tour = createTour({
|
|
363
|
+
brokerIntakeEnabled: false,
|
|
364
|
+
isBrokerReferral: true,
|
|
365
|
+
...brokerFields,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const submission = tour.submit();
|
|
369
|
+
|
|
370
|
+
expect(requests).to.have.length(1);
|
|
371
|
+
expect(JSON.parse(requests[0].requestBody ?? "{}")).not.to.have.any.keys(
|
|
372
|
+
"broker_first_name",
|
|
373
|
+
"broker_last_name",
|
|
374
|
+
"broker_email",
|
|
375
|
+
"broker_firm_name"
|
|
376
|
+
);
|
|
377
|
+
requests[0].respond(200, { "Content-Type": "application/json" }, "{}");
|
|
378
|
+
await submission;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("won't book a referral tour where the broker entered themselves as the client", async () => {
|
|
382
|
+
const requests = captureRequests();
|
|
383
|
+
const tour = createTour({
|
|
384
|
+
brokerIntakeEnabled: true,
|
|
385
|
+
isBrokerReferral: true,
|
|
386
|
+
...brokerFields,
|
|
387
|
+
brokerEmail: "CORA@example.com",
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await tour.submit();
|
|
391
|
+
|
|
392
|
+
expect(requests).to.have.length(0);
|
|
393
|
+
expect(tour.formIsValidForSubmission()).to.equal(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("won't book a referral tour without the broker's name", async () => {
|
|
397
|
+
const requests = captureRequests();
|
|
398
|
+
const tour = createTour({
|
|
399
|
+
brokerIntakeEnabled: true,
|
|
400
|
+
isBrokerReferral: true,
|
|
401
|
+
...brokerFields,
|
|
402
|
+
brokerFirstName: " ",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await tour.submit();
|
|
406
|
+
|
|
407
|
+
expect(requests).to.have.length(0);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("moves what a broker typed on the tour form into their own details when they tick the box", () => {
|
|
411
|
+
const tour = createTour({ brokerIntakeEnabled: true });
|
|
412
|
+
const harness = tour as unknown as TourHarness;
|
|
413
|
+
const toggle = (checked: boolean) =>
|
|
414
|
+
tour.onToggleBrokerReferral({ target: { checked } } as unknown as Event);
|
|
415
|
+
|
|
416
|
+
toggle(true);
|
|
417
|
+
|
|
418
|
+
expect([tour.brokerFirstName, tour.brokerEmail]).to.deep.equal([
|
|
419
|
+
"Cora",
|
|
420
|
+
"cora@example.com",
|
|
421
|
+
]);
|
|
422
|
+
expect([
|
|
423
|
+
tour.firstNameInputValue,
|
|
424
|
+
tour.lastNameInputValue,
|
|
425
|
+
harness.email,
|
|
426
|
+
harness.phoneNumberE164,
|
|
427
|
+
]).to.deep.equal(["", "", "", null]);
|
|
428
|
+
|
|
429
|
+
// Unticking before entering a client puts the details back.
|
|
430
|
+
toggle(false);
|
|
431
|
+
expect([tour.firstNameInputValue, harness.email]).to.deep.equal([
|
|
432
|
+
"Cora",
|
|
433
|
+
"cora@example.com",
|
|
434
|
+
]);
|
|
435
|
+
expect(tour.brokerEmail).to.equal("");
|
|
436
|
+
});
|