diff --git a/src/Gleap.js b/src/Gleap.js index 548c876d..fb38590c 100755 --- a/src/Gleap.js +++ b/src/Gleap.js @@ -1011,16 +1011,20 @@ class Gleap { return; } - const options = normalizeSurveyOptions(formatOrOptions); - Gleap.startFeedbackFlowWithOptions( - surveyId, - { - hideBackButton: true, - format: options.appMode, - survey: options, - }, - true - ); + // The messenger reads the contact once, when the survey starts, to skip the contact questions + // it already knows: wait for an identify / updateContact still in flight (capped). + sessionInstance.onContactSettled(() => { + const options = normalizeSurveyOptions(formatOrOptions); + Gleap.startFeedbackFlowWithOptions( + surveyId, + { + hideBackButton: true, + format: options.appMode, + survey: options, + }, + true + ); + }); } /** diff --git a/src/GleapFrameManager.js b/src/GleapFrameManager.js index db8ab648..c2275543 100644 --- a/src/GleapFrameManager.js +++ b/src/GleapFrameManager.js @@ -52,6 +52,8 @@ export default class GleapFrameManager { // Card and page surveys stay invisible until the messenger reports their height (no tall blank flash). surveyAwaitingHeight = false; surveyRevealTimeout = null; + // A freshly injected frame opens shortly after its first ping, unless it was closed meanwhile. + openAfterPingTimeout = null; // The messenger fell back to a legacy (pre Surveys 2.0) survey: use the old survey chrome. surveyLegacy = false; // Surveys 2.0 corners reported by the messenger (survey-theme): 'sharp' | 'pill', null = rounded. @@ -154,8 +156,12 @@ export default class GleapFrameManager { this.surveyDark = null; this.updateFrameStyle(); - // Wait for the survey's height before showing a card/page survey in a frame that has none yet. - this.surveyAwaitingHeight = this.isAutoHeightSurvey() && (!this.comReady || !this.frameHeight); + // A card/page survey waits for its own height before it shows, also in a frame that already + // measured an earlier survey (the messenger reports none for a survey with nothing to ask). + if (this.isAutoHeightSurvey()) { + this.frameHeight = 0; + } + this.surveyAwaitingHeight = this.isAutoHeightSurvey(); this.applyInnerSize(); // Lay the hidden frame out (invisibly) so the messenger can measure the survey right away. @@ -539,6 +545,8 @@ export default class GleapFrameManager { clearTimeout(this.surveyRevealTimeout); this.surveyRevealTimeout = null; } + clearTimeout(this.openAfterPingTimeout); + this.openAfterPingTimeout = null; this.gleapFrameContainer = null; this.gleapFrame = null; this.captureHidden = false; @@ -950,6 +958,15 @@ export default class GleapFrameManager { return; } + // A survey the messenger closes before it was shown (nothing to ask): it must not open after + // all, and there is nothing to animate and no close event without an open. + const wasOpened = this.widgetOpened; + const wasVisible = this.isOpened(); + if (this.openAfterPingTimeout) { + clearTimeout(this.openAfterPingTimeout); + this.openAfterPingTimeout = null; + } + this.hideMarkerManager(); this.setCaptureEditorOpen(false); if (this.gleapFrameContainer) { @@ -966,7 +983,7 @@ export default class GleapFrameManager { this.surveyRevealTimeout = null; } - if (container.classList.contains('gleap-frame-container--measuring')) { + if (!wasOpened || container.classList.contains('gleap-frame-container--measuring')) { // Never shown: nothing to animate. container.classList.remove('gleap-frame-container--measuring'); container.classList.add('gleap-frame-container--hidden'); @@ -989,7 +1006,9 @@ export default class GleapFrameManager { this.widgetOpened = false; this.updateWidgetStatus(); GleapFeedbackButtonManager.getInstance().updateFeedbackButtonState(); - GleapEventManager.notifyEvent('close'); + if (wasVisible) { + GleapEventManager.notifyEvent('close'); + } GleapNotificationManager.getInstance().reloadNotificationsFromCache(); this.unregisterEscListener(); @@ -1073,7 +1092,9 @@ export default class GleapFrameManager { this.sendSessionUpdate(); this.sendCaptureCapabilities(); this.workThroughQueue(); - setTimeout(() => { + clearTimeout(this.openAfterPingTimeout); + this.openAfterPingTimeout = setTimeout(() => { + this.openAfterPingTimeout = null; this.runWidgetShouldOpenCallback(); }, 300); } diff --git a/src/GleapSession.js b/src/GleapSession.js index 290bcc21..38207a6f 100644 --- a/src/GleapSession.js +++ b/src/GleapSession.js @@ -50,6 +50,12 @@ export default class GleapSession { // never re-identified from stale state. lastIdentify = null; identifyInFlight = false; + // identify / updateContact requests still waiting for the server. A survey + // started meanwhile would get the previous contact (Surveys 2.0 skips the + // contact questions an identified contact already answered), so showSurvey + // waits for them (onContactSettled). + pendingContactUpdates = 0; + contactSettledListeners = []; // GleapSession singleton static instance; @@ -147,6 +153,51 @@ export default class GleapSession { } }; + /** + * Counts an identify / updateContact request until it settles. The session + * it returns is applied (validateSession, session-update to the messenger) + * before the listeners run. + */ + trackContactUpdate = (request) => { + this.pendingContactUpdates++; + const settle = () => { + this.pendingContactUpdates = Math.max(0, this.pendingContactUpdates - 1); + if (this.pendingContactUpdates > 0) { + return; + } + const listeners = this.contactSettledListeners; + this.contactSettledListeners = []; + for (let i = 0; i < listeners.length; i++) { + listeners[i](); + } + }; + request.then(settle, settle); + return request; + }; + + /** + * Runs `callback` once no identify / updateContact request is in flight, or + * after `timeoutMs` at the latest. + */ + onContactSettled = (callback, timeoutMs = 5000) => { + if (this.pendingContactUpdates === 0) { + callback(); + return; + } + let done = false; + let timer = null; + const run = () => { + if (done) { + return; + } + done = true; + clearTimeout(timer); + callback(); + }; + timer = setTimeout(run, timeoutMs); + this.contactSettledListeners.push(run); + }; + injectSession = (http) => { if (http && this.session) { http.setRequestHeader('Api-Token', this.sdkKey); @@ -477,7 +528,7 @@ export default class GleapSession { } const self = this; - return new Promise((resolve, reject) => { + return this.trackContactUpdate(new Promise((resolve, reject) => { // Wait for gleap session to be ready. this.setOnSessionReady(function () { if (!self.session.gleapId || !self.session.gleapHash) { @@ -526,7 +577,7 @@ export default class GleapSession { }) ); }); - }); + })); }; identifySession = (userId, userData, userHash, refreshFileSession = false) => { @@ -541,7 +592,7 @@ export default class GleapSession { } const self = this; - return new Promise((resolve, reject) => { + return this.trackContactUpdate(new Promise((resolve, reject) => { // Wait for gleap session to be ready. this.setOnSessionReady(function () { if (!self.session.gleapId || !self.session.gleapHash) { @@ -603,7 +654,7 @@ export default class GleapSession { }) ); }); - }); + })); }; startProductTourConfig = (tourId) => {