@meetelise/chat 1.55.3 → 1.57.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/MyPubnub.d.ts +38 -3
- package/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +8 -2
- package/dist/src/WebComponent/actions/email-us-window.d.ts +7 -1
- package/dist/src/WebComponent/actions/international-phone-input.d.ts +1 -0
- package/dist/src/WebComponent/brokerReferral.d.ts +3 -1
- package/dist/src/WebComponent/me-chat.d.ts +33 -0
- package/dist/src/WebComponent/pubnub-chat.d.ts +3 -0
- package/dist/src/WebComponent/pubnub-navigation.d.ts +17 -0
- package/dist/src/webchatNavigation.d.ts +38 -0
- package/package.json +1 -1
- package/public/dist/index.js +223 -160
- package/src/MyPubnub.ts +87 -5
- package/src/WebComponent/Scheduler/tour-scheduler.ts +73 -9
- package/src/WebComponent/actions/email-us-window.ts +63 -9
- package/src/WebComponent/actions/international-phone-input.ts +4 -1
- package/src/WebComponent/brokerReferral.ts +6 -1
- package/src/WebComponent/me-chat.ts +158 -26
- package/src/WebComponent/pubnub-chat.ts +36 -10
- package/src/WebComponent/pubnub-navigation.ts +73 -0
- package/src/brokerReferralSubmission.test.ts +93 -6
- package/src/main/MEChat.ts +9 -1
- package/src/orgLevelNavigation.test.ts +625 -0
- package/src/webchatNavigation.test.ts +99 -0
- package/src/webchatNavigation.ts +108 -0
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
shouldOpenTourSchedulerFromUrl,
|
|
77
77
|
storeGoogleReserveReferral,
|
|
78
78
|
} from "../googleReserve";
|
|
79
|
+
import { consumeReopenChatAfterNavigation } from "../webchatNavigation";
|
|
79
80
|
|
|
80
81
|
@customElement("me-chat")
|
|
81
82
|
export class MEChat extends LitElement {
|
|
@@ -136,6 +137,19 @@ export class MEChat extends LitElement {
|
|
|
136
137
|
private orgSlug = "";
|
|
137
138
|
@property({ type: Boolean })
|
|
138
139
|
private centralConvo = false;
|
|
140
|
+
/**
|
|
141
|
+
* Org-level webchat only: the building the snippet names (`building` in
|
|
142
|
+
* `MEChat.start`). It doesn't change config or the conversation, which stay
|
|
143
|
+
* org-anchored; its id is sent with visitor messages as `declaredBuildingId`.
|
|
144
|
+
*/
|
|
145
|
+
@property({ type: String })
|
|
146
|
+
private declaredBuildingSlug = "";
|
|
147
|
+
/**
|
|
148
|
+
* Org-level webchat (`centralConvo` started without a building). Stays true
|
|
149
|
+
* after the widget adopts the org's triage general building from
|
|
150
|
+
* /webchat/init, which sets `buildingSlug`.
|
|
151
|
+
*/
|
|
152
|
+
private orgLevel = false;
|
|
139
153
|
@property({ type: Object })
|
|
140
154
|
launcherStyles: StyleInfo = {};
|
|
141
155
|
@property()
|
|
@@ -284,26 +298,30 @@ export class MEChat extends LitElement {
|
|
|
284
298
|
private setupWebchat = async (): Promise<void> => {
|
|
285
299
|
if (!this.orgSlug) return;
|
|
286
300
|
if (!this.centralConvo && !this.buildingSlug) return;
|
|
301
|
+
if (this.centralConvo && !this.buildingSlug) this.orgLevel = true;
|
|
287
302
|
const buildingWebchatView = this.centralConvo
|
|
288
303
|
? await fetchOrgCentralWebchatView(this.orgSlug)
|
|
289
304
|
: await fetchBuildingWebchatView(this.orgSlug, this.buildingSlug);
|
|
290
305
|
|
|
291
|
-
|
|
306
|
+
// Both post per-building data, keyed by the building slug.
|
|
307
|
+
if (!this.isOrgMode()) {
|
|
308
|
+
if (Math.random() < 0.2) {
|
|
309
|
+
try {
|
|
310
|
+
updateRentgrataToken(this.buildingSlug, this.orgSlug);
|
|
311
|
+
} catch (e) {
|
|
312
|
+
// eslint-disable-next-line no-console
|
|
313
|
+
console.warn("Error updating rentgrata token", e);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
292
317
|
try {
|
|
293
|
-
|
|
318
|
+
logCdnVersion(this.orgSlug, this.buildingSlug);
|
|
294
319
|
} catch (e) {
|
|
295
320
|
// eslint-disable-next-line no-console
|
|
296
|
-
console.warn("Error
|
|
321
|
+
console.warn("Error logging CDN version", e);
|
|
297
322
|
}
|
|
298
323
|
}
|
|
299
324
|
|
|
300
|
-
try {
|
|
301
|
-
logCdnVersion(this.orgSlug, this.buildingSlug);
|
|
302
|
-
} catch (e) {
|
|
303
|
-
// eslint-disable-next-line no-console
|
|
304
|
-
console.warn("Error logging CDN version", e);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
325
|
const shouldShowWebchat = await getShouldShowWebchat(
|
|
308
326
|
buildingWebchatView,
|
|
309
327
|
this.buildingSlug,
|
|
@@ -325,6 +343,9 @@ export class MEChat extends LitElement {
|
|
|
325
343
|
}
|
|
326
344
|
this.buildingWebchatView = buildingWebchatView;
|
|
327
345
|
|
|
346
|
+
// A "Take me there" click on the previous page.
|
|
347
|
+
const reopenAfterNavigation = consumeReopenChatAfterNavigation();
|
|
348
|
+
|
|
328
349
|
await this.initializeChatVariables();
|
|
329
350
|
await this.handleChatInitializeAnalytics();
|
|
330
351
|
await this.setBuildingDerivedInfo();
|
|
@@ -333,7 +354,13 @@ export class MEChat extends LitElement {
|
|
|
333
354
|
|
|
334
355
|
storeGoogleReserveReferral(this.buildingWebchatView?.id ?? 0);
|
|
335
356
|
|
|
336
|
-
if (
|
|
357
|
+
if (reopenAfterNavigation) {
|
|
358
|
+
// Reopen the chat the visitor was using. initializeChatVariables has
|
|
359
|
+
// already loaded the saved conversation, so it is drawn on open.
|
|
360
|
+
this.displayPubnubChat = true;
|
|
361
|
+
this.hideLauncher = true;
|
|
362
|
+
this.hasMounted = true;
|
|
363
|
+
} else if (
|
|
337
364
|
shouldOpenTourSchedulerFromUrl() &&
|
|
338
365
|
(this.enabledChatWidgets.sstDesktop || this.enabledChatWidgets.sstMobile)
|
|
339
366
|
) {
|
|
@@ -355,7 +382,9 @@ export class MEChat extends LitElement {
|
|
|
355
382
|
setBuildingDerivedInfo = async (): Promise<void> => {
|
|
356
383
|
const buildingDetails = this.buildingWebchatView;
|
|
357
384
|
if (!buildingDetails) return;
|
|
358
|
-
|
|
385
|
+
// Org-level webchat runs on the org's own pages, so it leaves their links,
|
|
386
|
+
// forms and phone numbers alone rather than pointing them at its building.
|
|
387
|
+
if (buildingDetails && this.orgSlug && !this.orgLevel) {
|
|
359
388
|
overrideContactUsForm(
|
|
360
389
|
this.orgSlug,
|
|
361
390
|
this.LeadSourceClient?.chatId ?? null,
|
|
@@ -385,7 +414,10 @@ export class MEChat extends LitElement {
|
|
|
385
414
|
bedroomPreferenceRequired,
|
|
386
415
|
brokerIntakeEnabled,
|
|
387
416
|
] = await Promise.all([
|
|
388
|
-
|
|
417
|
+
// Per-building endpoint; org mode has no building to ask about.
|
|
418
|
+
this.isOrgMode()
|
|
419
|
+
? Promise.resolve([])
|
|
420
|
+
: fetchLeadSources(this.orgSlug, this.buildingSlug),
|
|
389
421
|
fetchFeatureFlagUseApplicationsLinkReplacement(this.buildingSlug),
|
|
390
422
|
fetchBuildingAip(this.buildingSlug),
|
|
391
423
|
fetchTrackedRolloutBedroomPreferenceRequired(this.buildingSlug),
|
|
@@ -405,7 +437,10 @@ export class MEChat extends LitElement {
|
|
|
405
437
|
this.brokerIntakeEnabled = brokerIntakeEnabled;
|
|
406
438
|
|
|
407
439
|
// The backend is cached for ~4 hours, so falling back to the existing api request until the cache propagates all the changes
|
|
408
|
-
|
|
440
|
+
// Org-level webchat never offers Text Us, so it needs no number for it.
|
|
441
|
+
const buildingPhoneNumber = this.orgLevel
|
|
442
|
+
? null
|
|
443
|
+
: buildingDetails.textWithUsPhoneNumber
|
|
409
444
|
? buildingDetails.textWithUsPhoneNumber
|
|
410
445
|
: await getBuildingPhoneNumber(this.buildingSlug);
|
|
411
446
|
|
|
@@ -435,9 +470,15 @@ export class MEChat extends LitElement {
|
|
|
435
470
|
calcMobile: shouldShowCalcMobile,
|
|
436
471
|
};
|
|
437
472
|
|
|
438
|
-
this.enabledChatWidgets =
|
|
473
|
+
this.enabledChatWidgets = this.withOrgLevelContactOptions(
|
|
474
|
+
getWidgetOverrides(baseEnabledWidgets),
|
|
475
|
+
this.buildingWebchatView?.phoneNumber ?? ""
|
|
476
|
+
);
|
|
439
477
|
|
|
440
|
-
if (
|
|
478
|
+
if (this.orgLevel) {
|
|
479
|
+
// Org-level webchat leaves the org site's tour links alone too; they'd
|
|
480
|
+
// otherwise open the triage general building's scheduler.
|
|
481
|
+
} else if (
|
|
441
482
|
this.enabledChatWidgets.sstDesktop ||
|
|
442
483
|
this.enabledChatWidgets.sstMobile ||
|
|
443
484
|
this.orgSlug === "radco"
|
|
@@ -501,7 +542,8 @@ export class MEChat extends LitElement {
|
|
|
501
542
|
|
|
502
543
|
const buildingsPhoneNumber = this.buildingWebchatView?.phoneNumber ?? "";
|
|
503
544
|
let phoneNumberForSource = null;
|
|
504
|
-
|
|
545
|
+
// Org-level webchat calls its building's own number, never a DNI number.
|
|
546
|
+
if (featureFlagUseDNI && !this.orgLevel) {
|
|
505
547
|
phoneNumberForSource = await fetchPhoneNumberFromSource(
|
|
506
548
|
this.buildingSlug,
|
|
507
549
|
this.LeadSourceClient?.leadSource ?? null
|
|
@@ -560,12 +602,18 @@ export class MEChat extends LitElement {
|
|
|
560
602
|
this.leadSources.push(currentLeadSource);
|
|
561
603
|
}
|
|
562
604
|
}
|
|
563
|
-
|
|
605
|
+
if (!this.isOrgMode()) {
|
|
606
|
+
getRawAvailabilities(buildingDetails.id); // we're not using this here, just want to cache the result
|
|
607
|
+
}
|
|
564
608
|
const isApplicationLinkReplacementEnabled =
|
|
565
609
|
(featureFlagUseApplicationsLinkReplacement ||
|
|
566
610
|
this.buildingWebchatView?.isAip) &&
|
|
567
611
|
this.buildingWebchatView?.isLiveOnApplications;
|
|
568
|
-
if (
|
|
612
|
+
if (
|
|
613
|
+
isApplicationLinkReplacementEnabled &&
|
|
614
|
+
this.buildingSlug &&
|
|
615
|
+
!this.orgLevel
|
|
616
|
+
) {
|
|
569
617
|
const totalReplacements = replaceSelectButtonsWithNewLink(
|
|
570
618
|
`https://applications.eliseai.com/building/${this.buildingSlug}/units`,
|
|
571
619
|
this.orgSlug,
|
|
@@ -586,7 +634,7 @@ export class MEChat extends LitElement {
|
|
|
586
634
|
}
|
|
587
635
|
}
|
|
588
636
|
|
|
589
|
-
if (this.buildingSlug && this.orgSlug) {
|
|
637
|
+
if (this.buildingSlug && this.orgSlug && !this.orgLevel) {
|
|
590
638
|
try {
|
|
591
639
|
replaceOutdatedApplicationLinks(this.buildingSlug);
|
|
592
640
|
} catch {
|
|
@@ -597,6 +645,7 @@ export class MEChat extends LitElement {
|
|
|
597
645
|
|
|
598
646
|
if (
|
|
599
647
|
this.buildingSlug &&
|
|
648
|
+
!this.orgLevel &&
|
|
600
649
|
!!this.buildingWebchatView?.isLiveOnApplications &&
|
|
601
650
|
this.buildingWebchatView?.applicationLink &&
|
|
602
651
|
shouldOverwriteCheckAvail(this.orgSlug)
|
|
@@ -613,9 +662,10 @@ export class MEChat extends LitElement {
|
|
|
613
662
|
if (!this.buildingWebchatView) return;
|
|
614
663
|
|
|
615
664
|
// important to distinguish between a user who has never opened the chat and a user who has opened the chat but closed it
|
|
616
|
-
const
|
|
665
|
+
const chatStorageSlug = this.chatStorageSlug();
|
|
666
|
+
const currentChatStorageKey = getChatStorageKey(chatStorageSlug);
|
|
617
667
|
const isReturningUser = isChatKeyValid(
|
|
618
|
-
|
|
668
|
+
chatStorageSlug,
|
|
619
669
|
currentChatStorageKey
|
|
620
670
|
);
|
|
621
671
|
|
|
@@ -623,7 +673,7 @@ export class MEChat extends LitElement {
|
|
|
623
673
|
// temporary client-generated one (will be overwritten by server session below).
|
|
624
674
|
const updatedChatStorageKey = isReturningUser
|
|
625
675
|
? currentChatStorageKey
|
|
626
|
-
: createChatStorageKey(
|
|
676
|
+
: createChatStorageKey(chatStorageSlug, false);
|
|
627
677
|
|
|
628
678
|
if (!updatedChatStorageKey.leadId) {
|
|
629
679
|
throw new Error("Lead ID is null");
|
|
@@ -646,6 +696,17 @@ export class MEChat extends LitElement {
|
|
|
646
696
|
this.LeadSourceClient,
|
|
647
697
|
this.widgetType
|
|
648
698
|
);
|
|
699
|
+
this.myPubnub.chatStorageSlug = chatStorageSlug;
|
|
700
|
+
if (this.centralConvo) {
|
|
701
|
+
const myPubnub = this.myPubnub;
|
|
702
|
+
myPubnub.centralConvo = true;
|
|
703
|
+
// Not awaited: only needed once the visitor sends a message.
|
|
704
|
+
void this.fetchDeclaredBuildingId().then((id) => {
|
|
705
|
+
myPubnub.declaredBuildingId = id;
|
|
706
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
this.myPubnub.declaredBuildingId = this.buildingWebchatView.id;
|
|
709
|
+
}
|
|
649
710
|
|
|
650
711
|
// Always create/resume a server-side session so the client has a fresh
|
|
651
712
|
// sessionToken and never falls back to the insecure V1 token endpoint.
|
|
@@ -658,10 +719,13 @@ export class MEChat extends LitElement {
|
|
|
658
719
|
if (session) {
|
|
659
720
|
// Persist the server-provided leadId to localStorage, preserving initiatedChat flag
|
|
660
721
|
createChatStorageKey(
|
|
661
|
-
|
|
722
|
+
chatStorageSlug,
|
|
662
723
|
updatedChatStorageKey.initiatedChat,
|
|
663
724
|
session.lead_id
|
|
664
725
|
);
|
|
726
|
+
if (this.isOrgMode() && session.building_slug) {
|
|
727
|
+
await this.adoptBuilding(session.building_slug);
|
|
728
|
+
}
|
|
665
729
|
}
|
|
666
730
|
|
|
667
731
|
this.LeadSourceClient.chatId = this.myPubnub.channel;
|
|
@@ -693,12 +757,77 @@ export class MEChat extends LitElement {
|
|
|
693
757
|
}
|
|
694
758
|
// Re-read from localStorage so initializePubnub gets the server-provided
|
|
695
759
|
// leadId (not the stale reference captured before initSession).
|
|
696
|
-
const finalChatStorageKey = getChatStorageKey(
|
|
760
|
+
const finalChatStorageKey = getChatStorageKey(chatStorageSlug);
|
|
697
761
|
if (finalChatStorageKey.initiatedChat) {
|
|
698
762
|
await this.myPubnub.initializePubnub(finalChatStorageKey);
|
|
699
763
|
}
|
|
700
764
|
};
|
|
701
765
|
|
|
766
|
+
/**
|
|
767
|
+
* Org-level webchat with no building: config comes from the org, and there is
|
|
768
|
+
* no building slug for the per-building endpoints.
|
|
769
|
+
*/
|
|
770
|
+
private isOrgMode = (): boolean => this.centralConvo && !this.buildingSlug;
|
|
771
|
+
|
|
772
|
+
/** Where the conversation is saved: org-level webchat keeps one per org. */
|
|
773
|
+
private chatStorageSlug = (): string =>
|
|
774
|
+
this.orgLevel ? "" : this.buildingSlug;
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Org-level webchat: /webchat/init chose the org's triage general building,
|
|
778
|
+
* so run as a widget for that building from here on (its config, and its
|
|
779
|
+
* slug for the per-building endpoints). The conversation stays the org-level
|
|
780
|
+
* one /webchat/init returned. If its config can't be loaded, stay in org mode.
|
|
781
|
+
*/
|
|
782
|
+
private adoptBuilding = async (buildingSlug: string): Promise<void> => {
|
|
783
|
+
try {
|
|
784
|
+
const view = await fetchBuildingWebchatView(this.orgSlug, buildingSlug);
|
|
785
|
+
if (!view?.id) return;
|
|
786
|
+
this.buildingSlug = buildingSlug;
|
|
787
|
+
this.buildingWebchatView = view;
|
|
788
|
+
this.myPubnub?.adoptBuilding(buildingSlug, view);
|
|
789
|
+
} catch (e) {
|
|
790
|
+
// eslint-disable-next-line no-console
|
|
791
|
+
console.warn("Error loading the org-level webchat building", e);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Org-level webchat offers no Email or Text Us, and offers Call Us only with
|
|
797
|
+
* its building's number (the org's triage line).
|
|
798
|
+
*/
|
|
799
|
+
private withOrgLevelContactOptions = <
|
|
800
|
+
T extends typeof this.enabledChatWidgets
|
|
801
|
+
>(
|
|
802
|
+
widgets: T,
|
|
803
|
+
callNumber: string
|
|
804
|
+
): T => {
|
|
805
|
+
if (!this.orgLevel) return widgets;
|
|
806
|
+
return {
|
|
807
|
+
...widgets,
|
|
808
|
+
emailDesktop: false,
|
|
809
|
+
emailMobile: false,
|
|
810
|
+
textDesktop: false,
|
|
811
|
+
textMobile: false,
|
|
812
|
+
callDesktop: widgets.callDesktop && !!callNumber,
|
|
813
|
+
callMobile: widgets.callMobile && !!callNumber,
|
|
814
|
+
};
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
/** The numeric id of the snippet's building in org-level webchat, if it names one. */
|
|
818
|
+
private fetchDeclaredBuildingId = async (): Promise<number | null> => {
|
|
819
|
+
if (!this.declaredBuildingSlug || !this.orgSlug) return null;
|
|
820
|
+
try {
|
|
821
|
+
const view = await fetchBuildingWebchatView(
|
|
822
|
+
this.orgSlug,
|
|
823
|
+
this.declaredBuildingSlug
|
|
824
|
+
);
|
|
825
|
+
return typeof view?.id === "number" && view.id > 0 ? view.id : null;
|
|
826
|
+
} catch {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
|
|
702
831
|
private shouldAutoOpenChat = (buildingHasAutoOpen: boolean): boolean => {
|
|
703
832
|
const alreadyAutoOpenedTimestamp = sessionStorage.getItem(
|
|
704
833
|
"alreadyAutoOpenedTimestamp"
|
|
@@ -728,7 +857,9 @@ export class MEChat extends LitElement {
|
|
|
728
857
|
|
|
729
858
|
public async restartConversation(): Promise<void> {
|
|
730
859
|
this.myPubnub?.handleDisconnect();
|
|
731
|
-
clearChatStorageKey(this.
|
|
860
|
+
clearChatStorageKey(this.chatStorageSlug());
|
|
861
|
+
// Org-level webchat asks /webchat/init for its building again.
|
|
862
|
+
if (this.orgLevel) this.buildingSlug = "";
|
|
732
863
|
this.myPubnub = null;
|
|
733
864
|
this.displayPubnubChat = false;
|
|
734
865
|
await this.setupWebchat();
|
|
@@ -981,6 +1112,7 @@ export class MEChat extends LitElement {
|
|
|
981
1112
|
.myPubnub=${this.myPubnub}
|
|
982
1113
|
.orgSlug=${this.orgSlug}
|
|
983
1114
|
.buildingSlug=${this.buildingSlug}
|
|
1115
|
+
.centralConvo=${this.centralConvo}
|
|
984
1116
|
.widgetType=${WidgetType.Default}
|
|
985
1117
|
.building=${this.buildingWebchatView}
|
|
986
1118
|
.primaryColor=${this.primaryColor}
|
|
@@ -6,6 +6,7 @@ import MyPubnub, {
|
|
|
6
6
|
SimpleMessageTypes,
|
|
7
7
|
SimpleChatMessage,
|
|
8
8
|
isSimpleMediaChatMessage,
|
|
9
|
+
isSimpleNavigationChatMessage,
|
|
9
10
|
isSimpleTextChatMessage,
|
|
10
11
|
} from "../MyPubnub";
|
|
11
12
|
import {
|
|
@@ -21,6 +22,7 @@ import { leasingChatStyles } from "./leasing-chat-styles";
|
|
|
21
22
|
import { pubnubChatStyles } from "./pubnub-chat-styles";
|
|
22
23
|
import "./pubnub-message";
|
|
23
24
|
import "./pubnub-media";
|
|
25
|
+
import "./pubnub-navigation";
|
|
24
26
|
import classNames from "classnames";
|
|
25
27
|
import { differenceInDays } from "date-fns";
|
|
26
28
|
import { WidgetType } from "../main/MEChat";
|
|
@@ -79,6 +81,10 @@ export class PubnubChat extends LitElement {
|
|
|
79
81
|
@property({ attribute: true })
|
|
80
82
|
buildingSlug: string | undefined;
|
|
81
83
|
|
|
84
|
+
/** Org-level webchat, which has no building slug. */
|
|
85
|
+
@property({ attribute: true })
|
|
86
|
+
centralConvo = false;
|
|
87
|
+
|
|
82
88
|
@property({ attribute: true })
|
|
83
89
|
widgetType: WidgetType = WidgetType.Default;
|
|
84
90
|
|
|
@@ -243,7 +249,7 @@ export class PubnubChat extends LitElement {
|
|
|
243
249
|
};
|
|
244
250
|
|
|
245
251
|
render(): TemplateResult {
|
|
246
|
-
if (!this.buildingSlug) {
|
|
252
|
+
if (!this.buildingSlug && !this.centralConvo) {
|
|
247
253
|
return html``; // error should be here
|
|
248
254
|
}
|
|
249
255
|
|
|
@@ -376,10 +382,14 @@ export class PubnubChat extends LitElement {
|
|
|
376
382
|
role="article"
|
|
377
383
|
aria-label="${message.isLeadMessage
|
|
378
384
|
? "You"
|
|
379
|
-
: "Elise"}: ${
|
|
380
|
-
? message.message
|
|
381
|
-
: "Media message"}"
|
|
385
|
+
: "Elise"}: ${messageAriaLabel(message)}"
|
|
382
386
|
>
|
|
387
|
+
${isSimpleNavigationChatMessage(message)
|
|
388
|
+
? html`<pubnub-navigation
|
|
389
|
+
.message=${message}
|
|
390
|
+
.onMount=${() => this.scrollToChatBottom()}
|
|
391
|
+
></pubnub-navigation>`
|
|
392
|
+
: ""}
|
|
383
393
|
${isSimpleMediaChatMessage(message)
|
|
384
394
|
? html` <pubnub-media
|
|
385
395
|
.message=${message}
|
|
@@ -551,10 +561,14 @@ export class PubnubChat extends LitElement {
|
|
|
551
561
|
role="article"
|
|
552
562
|
aria-label="${message.isLeadMessage
|
|
553
563
|
? "You"
|
|
554
|
-
: "Elise"}: ${
|
|
555
|
-
? message.message
|
|
556
|
-
: "Media message"}"
|
|
564
|
+
: "Elise"}: ${messageAriaLabel(message)}"
|
|
557
565
|
>
|
|
566
|
+
${isSimpleNavigationChatMessage(message)
|
|
567
|
+
? html`<pubnub-navigation
|
|
568
|
+
.message=${message}
|
|
569
|
+
.onMount=${() => this.scrollToChatBottom()}
|
|
570
|
+
></pubnub-navigation>`
|
|
571
|
+
: ""}
|
|
558
572
|
${isSimpleMediaChatMessage(message)
|
|
559
573
|
? html` <pubnub-media
|
|
560
574
|
.message=${message}
|
|
@@ -750,10 +764,14 @@ export class PubnubChat extends LitElement {
|
|
|
750
764
|
role="article"
|
|
751
765
|
aria-label="${message.isLeadMessage
|
|
752
766
|
? "You"
|
|
753
|
-
: "Elise"}: ${
|
|
754
|
-
? message.message
|
|
755
|
-
: "Media message"}"
|
|
767
|
+
: "Elise"}: ${messageAriaLabel(message)}"
|
|
756
768
|
>
|
|
769
|
+
${isSimpleNavigationChatMessage(message)
|
|
770
|
+
? html`<pubnub-navigation
|
|
771
|
+
.message=${message}
|
|
772
|
+
.onMount=${() => this.scrollToChatBottom()}
|
|
773
|
+
></pubnub-navigation>`
|
|
774
|
+
: ""}
|
|
757
775
|
${isSimpleMediaChatMessage(message)
|
|
758
776
|
? html` <pubnub-media
|
|
759
777
|
.message=${message}
|
|
@@ -851,3 +869,11 @@ export class PubnubChat extends LitElement {
|
|
|
851
869
|
`;
|
|
852
870
|
}
|
|
853
871
|
}
|
|
872
|
+
|
|
873
|
+
const messageAriaLabel = (message: SimpleChatMessage): string => {
|
|
874
|
+
if (isSimpleTextChatMessage(message)) return message.message;
|
|
875
|
+
if (isSimpleNavigationChatMessage(message)) {
|
|
876
|
+
return `Take me to ${message.navigation.label}`;
|
|
877
|
+
}
|
|
878
|
+
return "Media message";
|
|
879
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { css, html, LitElement, TemplateResult } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
4
|
+
import { SimpleNavigationChatMessage } from "../MyPubnub";
|
|
5
|
+
import { isMobile } from "../utils";
|
|
6
|
+
import { takeMeThere } from "../webchatNavigation";
|
|
7
|
+
import { InputStyles } from "./actions/InputStyles";
|
|
8
|
+
import { pubnubChatStyles } from "./pubnub-chat-styles";
|
|
9
|
+
|
|
10
|
+
/** The "Take me to {label}" button the agent sends after a reply. */
|
|
11
|
+
@customElement("pubnub-navigation")
|
|
12
|
+
export class PubnubNavigation extends LitElement {
|
|
13
|
+
static styles = [
|
|
14
|
+
InputStyles,
|
|
15
|
+
pubnubChatStyles,
|
|
16
|
+
css`
|
|
17
|
+
.navigation-button {
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
border: none;
|
|
20
|
+
border-radius: 8px;
|
|
21
|
+
padding: 8px 12px;
|
|
22
|
+
background: black;
|
|
23
|
+
color: white;
|
|
24
|
+
font-family: inherit;
|
|
25
|
+
text-align: left;
|
|
26
|
+
line-height: 1.4;
|
|
27
|
+
}
|
|
28
|
+
.navigation-button:hover {
|
|
29
|
+
opacity: 0.85;
|
|
30
|
+
}
|
|
31
|
+
.navigation-button:focus-visible {
|
|
32
|
+
outline: 2px solid #4d90fe;
|
|
33
|
+
outline-offset: 2px;
|
|
34
|
+
}
|
|
35
|
+
`,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
@property({ attribute: false })
|
|
39
|
+
message: SimpleNavigationChatMessage | undefined;
|
|
40
|
+
|
|
41
|
+
@property({ attribute: false })
|
|
42
|
+
onMount: () => void = () => ({});
|
|
43
|
+
|
|
44
|
+
/** Overridable in tests; navigates the same tab by default. */
|
|
45
|
+
@property({ attribute: false })
|
|
46
|
+
navigate: (url: string) => void = (url) => window.location.assign(url);
|
|
47
|
+
|
|
48
|
+
updated(): void {
|
|
49
|
+
this.onMount();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render(): TemplateResult {
|
|
53
|
+
if (!this.message) return html``;
|
|
54
|
+
const { navigation } = this.message;
|
|
55
|
+
return html`<button
|
|
56
|
+
type="button"
|
|
57
|
+
class=${classMap({
|
|
58
|
+
["navigation-button"]: true,
|
|
59
|
+
["webchat-font__desktop"]: !isMobile(),
|
|
60
|
+
["webchat-font__mobile"]: isMobile(),
|
|
61
|
+
})}
|
|
62
|
+
@click=${() => takeMeThere(navigation, this.navigate)}
|
|
63
|
+
>
|
|
64
|
+
Take me to ${navigation.label}
|
|
65
|
+
</button>`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
declare global {
|
|
70
|
+
interface HTMLElementTagNameMap {
|
|
71
|
+
"pubnub-navigation": PubnubNavigation;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -85,6 +85,43 @@ it("sends a broker referral with the client as the prospect and the broker field
|
|
|
85
85
|
expect(JSON.stringify(event)).not.to.contain("bea@realty.example.com");
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it("sends the broker's phone on a referral email when they gave one", async () => {
|
|
89
|
+
const requests = captureRequests();
|
|
90
|
+
const emailUs = createForm({
|
|
91
|
+
brokerIntakeEnabled: true,
|
|
92
|
+
isBrokerReferral: true,
|
|
93
|
+
...brokerFields,
|
|
94
|
+
brokerPhone: "(312) 555-0199",
|
|
95
|
+
brokerPhoneE164: "+13125550199",
|
|
96
|
+
brokerPhoneIsValid: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const submission = emailUs.onClick();
|
|
100
|
+
|
|
101
|
+
expect(requests).to.have.length(1);
|
|
102
|
+
expect(JSON.parse(requests[0].requestBody ?? "{}")).to.include({
|
|
103
|
+
broker_phone: "+13125550199",
|
|
104
|
+
});
|
|
105
|
+
requests[0].respond(200, { "Content-Type": "application/json" }, "{}");
|
|
106
|
+
await submission;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("won't send a referral email with an invalid broker phone", async () => {
|
|
110
|
+
const requests = captureRequests();
|
|
111
|
+
const emailUs = createForm({
|
|
112
|
+
brokerIntakeEnabled: true,
|
|
113
|
+
isBrokerReferral: true,
|
|
114
|
+
...brokerFields,
|
|
115
|
+
brokerPhone: "555",
|
|
116
|
+
brokerPhoneE164: null,
|
|
117
|
+
brokerPhoneIsValid: false,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await emailUs.onClick();
|
|
121
|
+
|
|
122
|
+
expect(requests).to.have.length(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
88
125
|
it("sends no broker fields when broker intake is off, whatever the form holds", async () => {
|
|
89
126
|
const requests = captureRequests();
|
|
90
127
|
const emailUs = createForm({
|
|
@@ -224,9 +261,15 @@ it("moves what a broker typed into their own details when they tick the referral
|
|
|
224
261
|
fill("Email", "bea@realty.example.com");
|
|
225
262
|
await emailUs.updateComplete;
|
|
226
263
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
264
|
+
// The switch moves when ticked (under Brokerage), so find it again for each click.
|
|
265
|
+
const checkbox = {
|
|
266
|
+
click: () =>
|
|
267
|
+
(
|
|
268
|
+
emailUs.shadowRoot?.querySelector(
|
|
269
|
+
"#broker-referral-toggle"
|
|
270
|
+
) as HTMLInputElement
|
|
271
|
+
).click(),
|
|
272
|
+
};
|
|
230
273
|
checkbox.click();
|
|
231
274
|
await emailUs.updateComplete;
|
|
232
275
|
|
|
@@ -257,9 +300,15 @@ it("keeps a filled client when the referral box is unticked and ticked again", a
|
|
|
257
300
|
Object.assign(emailUs, { brokerIntakeEnabled: true, buildingId: 445038 });
|
|
258
301
|
document.body.appendChild(emailUs);
|
|
259
302
|
await emailUs.updateComplete;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
303
|
+
// The switch moves when ticked (under Brokerage), so find it again for each click.
|
|
304
|
+
const checkbox = {
|
|
305
|
+
click: () =>
|
|
306
|
+
(
|
|
307
|
+
emailUs.shadowRoot?.querySelector(
|
|
308
|
+
"#broker-referral-toggle"
|
|
309
|
+
) as HTMLInputElement
|
|
310
|
+
).click(),
|
|
311
|
+
};
|
|
263
312
|
checkbox.click();
|
|
264
313
|
await emailUs.updateComplete;
|
|
265
314
|
Object.assign(emailUs, {
|
|
@@ -357,6 +406,44 @@ it("books a broker's tour with the client as the prospect and the broker fields"
|
|
|
357
406
|
expect(JSON.stringify(event)).not.to.contain("bea@realty.example.com");
|
|
358
407
|
});
|
|
359
408
|
|
|
409
|
+
it("sends the broker's phone on a referral tour when they gave one", async () => {
|
|
410
|
+
const requests = captureRequests();
|
|
411
|
+
const tour = createTour({
|
|
412
|
+
brokerIntakeEnabled: true,
|
|
413
|
+
isBrokerReferral: true,
|
|
414
|
+
...brokerFields,
|
|
415
|
+
brokerPhone: "(312) 555-0199",
|
|
416
|
+
brokerPhoneE164: "+13125550199",
|
|
417
|
+
brokerPhoneIsValid: true,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const submission = tour.submit();
|
|
421
|
+
|
|
422
|
+
expect(requests).to.have.length(1);
|
|
423
|
+
expect(JSON.parse(requests[0].requestBody ?? "{}")).to.include({
|
|
424
|
+
phone_number: "+13125550100",
|
|
425
|
+
broker_phone: "+13125550199",
|
|
426
|
+
});
|
|
427
|
+
requests[0].respond(200, { "Content-Type": "application/json" }, "{}");
|
|
428
|
+
await submission;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("won't book a referral tour with an invalid broker phone", async () => {
|
|
432
|
+
const requests = captureRequests();
|
|
433
|
+
const tour = createTour({
|
|
434
|
+
brokerIntakeEnabled: true,
|
|
435
|
+
isBrokerReferral: true,
|
|
436
|
+
...brokerFields,
|
|
437
|
+
brokerPhone: "555",
|
|
438
|
+
brokerPhoneE164: null,
|
|
439
|
+
brokerPhoneIsValid: false,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
await tour.submit();
|
|
443
|
+
|
|
444
|
+
expect(requests).to.have.length(0);
|
|
445
|
+
});
|
|
446
|
+
|
|
360
447
|
it("books a tour with no broker fields when broker intake is off, whatever the form holds", async () => {
|
|
361
448
|
const requests = captureRequests();
|
|
362
449
|
const tour = createTour({
|
package/src/main/MEChat.ts
CHANGED
|
@@ -66,7 +66,11 @@ export interface StudioWebchatContext {
|
|
|
66
66
|
|
|
67
67
|
export interface StudioWebchatClearContext {
|
|
68
68
|
source?: "elise_studio";
|
|
69
|
-
reason?:
|
|
69
|
+
reason?:
|
|
70
|
+
| "no_property_for_route"
|
|
71
|
+
| "webchat_disabled"
|
|
72
|
+
| "ambiguous_route"
|
|
73
|
+
| string;
|
|
70
74
|
preserveCache?: boolean;
|
|
71
75
|
}
|
|
72
76
|
|
|
@@ -209,6 +213,10 @@ export default class MEChat {
|
|
|
209
213
|
// Centralized webchat is org-anchored: config comes from the org, not a
|
|
210
214
|
// per-building slug or site resolution.
|
|
211
215
|
meChatElement.setAttribute("centralConvo", "true");
|
|
216
|
+
if (opts.building) {
|
|
217
|
+
// Page context only: sent with visitor messages as declaredBuildingId.
|
|
218
|
+
meChatElement.setAttribute("declaredBuildingSlug", opts.building);
|
|
219
|
+
}
|
|
212
220
|
} else {
|
|
213
221
|
this.handleBuildingslug(
|
|
214
222
|
meChatElement as MEChatLitElement,
|