@meetelise/chat 1.54.1 → 1.55.1

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.
Files changed (25) hide show
  1. package/dist/src/WebComponent/FeeCalculator/components/charge-inputs/charge-inputs.d.ts +3 -0
  2. package/dist/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.d.ts +4 -1
  3. package/dist/src/WebComponent/FeeCalculator/fee-calculator.d.ts +3 -0
  4. package/dist/src/WebComponent/FeeCalculator/model/marketable-fee.d.ts +2 -0
  5. package/dist/src/WebComponent/FeeCalculator/utils/pet-limit-utils.d.ts +10 -0
  6. package/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +21 -0
  7. package/dist/src/WebComponent/actions/email-us-window.d.ts +1 -7
  8. package/dist/src/WebComponent/brokerReferral.d.ts +17 -0
  9. package/dist/src/services/fees/calculateQuote.d.ts +14 -1
  10. package/package.json +1 -1
  11. package/public/dist/index.js +505 -284
  12. package/src/WebComponent/FeeCalculator/components/charge-inputs/charge-inputs.ts +44 -10
  13. package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout-styles.ts +7 -0
  14. package/src/WebComponent/FeeCalculator/components/fee-calculator-layout/fee-calculator-layout.ts +32 -1
  15. package/src/WebComponent/FeeCalculator/fee-calculator.ts +26 -11
  16. package/src/WebComponent/FeeCalculator/model/marketable-fee.ts +3 -0
  17. package/src/WebComponent/FeeCalculator/utils/pet-limit-utils.ts +27 -0
  18. package/src/WebComponent/Scheduler/tour-scheduler.ts +253 -32
  19. package/src/WebComponent/Scheduler/tourSchedulerStyles.ts +67 -1
  20. package/src/WebComponent/actions/email-us-window.ts +72 -51
  21. package/src/WebComponent/brokerReferral.ts +55 -0
  22. package/src/WebComponent/launcher/Launcher.ts +1 -0
  23. package/src/brokerReferralSubmission.test.ts +149 -0
  24. package/src/feeCalculatorPetLimit.test.ts +346 -0
  25. package/src/services/fees/calculateQuote.ts +38 -3
@@ -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
- gap: 6px;
106
- font-family: "Helvetica", Arial;
107
- font-size: 13px;
108
- color: #5c5c5c;
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
- firstName: this.brokerFirstName.trim(),
336
- lastName: this.brokerLastName.trim(),
337
- email: this.brokerEmail.trim(),
338
- firmName: this.brokerFirmName.trim(),
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
- if (!broker.firstName || !broker.lastName) {
353
- this.hasBrokerNameError = true;
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
- // The prospect fields above are the client's; broker intake links the broker to them.
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
+ });