From f7d5197052b2e7fad4d43fcdde2dc73d7d15fc5b Mon Sep 17 00:00:00 2001 From: Justin Chou <111300235+justinchou-google@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:46:09 -0400 Subject: [PATCH] Cleans up deprecated autopromptmanager flow. (#3495) * save point * clean up more * save point * save point * save point, fixed miniprompt and refactored cta tests * remove deprecated suites * save point, restore prompt prefrence, add displaydelay test * fix miniprompt test for basicruntime * remove extra console log statement * clean up lcoked references * adds basic runtime test for miniprompt disablement * remove console statements * add suite for localstorage timestamps via client event handler --------- Co-authored-by: justinchou-google --- src/runtime/auto-prompt-manager-test.js | 7183 ++++------------------- src/runtime/auto-prompt-manager.ts | 810 +-- src/runtime/basic-runtime-test.js | 129 +- src/utils/constants.ts | 49 +- 4 files changed, 1499 insertions(+), 6672 deletions(-) diff --git a/src/runtime/auto-prompt-manager-test.js b/src/runtime/auto-prompt-manager-test.js index b8de736b88..26718bf994 100644 --- a/src/runtime/auto-prompt-manager-test.js +++ b/src/runtime/auto-prompt-manager-test.js @@ -24,11 +24,6 @@ import {ClientConfig, UiPredicates} from '../model/client-config'; import {ClientConfigManager} from './client-config-manager'; import {ClientEventManager} from './client-event-manager'; import {ConfiguredRuntime} from './runtime'; -import { - Constants, - ImpressionStorageKeys, - StorageKeys, -} from '../utils/constants'; import {Entitlements} from '../api/entitlements'; import {EntitlementsManager} from './entitlements-manager'; import {ExperimentFlags} from './experiment-flags'; @@ -37,6 +32,7 @@ import {MiniPromptApi} from './mini-prompt-api'; import {MockDeps} from '../../test/mock-deps'; import {PageConfig} from '../model/page-config'; import {Storage} from './storage'; +import {StorageKeys} from '../utils/constants'; import {XhrFetcher} from './fetcher'; import {setExperiment} from './experiments'; import {tick} from '../../test/tick'; @@ -78,6 +74,7 @@ const REWARDED_AD_INTERVENTION = { describes.realWin('AutoPromptManager', (env) => { let autoPromptManager; let win; + let winMock; let deps; let doc; let pageConfig; @@ -105,6 +102,7 @@ describes.realWin('AutoPromptManager', (env) => { sandbox.useFakeTimers(CURRENT_TIME); win = Object.assign({}, env.win, {gtag: () => {}, ga: () => {}}); win.setTimeout = (callback) => callback(); + winMock = sandbox.mock(win); sandbox.stub(deps, 'win').returns(win); doc = new GlobalDoc(win); @@ -144,7 +142,6 @@ describes.realWin('AutoPromptManager', (env) => { sandbox.stub(MiniPromptApi.prototype, 'init'); autoPromptManager = new AutoPromptManager(deps, runtime); - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = true; miniPromptApiMock = sandbox.mock(autoPromptManager.miniPromptAPI_); @@ -162,5218 +159,1163 @@ describes.realWin('AutoPromptManager', (env) => { miniPromptApiMock.verify(); }); - function setWinWithAnalytics(gtag, ga) { - const winWithAnalytics = Object.assign({}, win); - if (!gtag) { - delete winWithAnalytics.gtag; - } - if (!ga) { - delete winWithAnalytics.ga; - } - autoPromptManager.deps_.win.restore(); - sandbox.stub(autoPromptManager.deps_, 'win').returns(winWithAnalytics); - } - it('should be listening for events from the events manager', () => { expect(eventManagerCallback).to.not.be.null; }); - it('should locally store contribution impressions when contribution impression events are fired', async () => { - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); + describe('LocalStorage timestamps via Client Event handler', () => { + it('should ignore undefined events', async () => { + autoPromptManager.isClosable_ = true; + storageMock.expects('get').never(); + storageMock.expects('set').never(); - it('should locally store subscription impressions when subscription impression events are fired', async () => { - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, + await eventManagerCallback({ + eventType: undefined, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); - }); - [ - { - miniPromptEventType: - AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, - largePromptEventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - dismissableEventType: AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED, - autoPromptType: AutoPromptType.CONTRIBUTION, - }, - { - miniPromptEventType: - AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT, - largePromptEventType: AnalyticsEvent.IMPRESSION_OFFERS, - dismissableEventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, - autoPromptType: AutoPromptType.SUBSCRIPTION, - }, - ].forEach((params) => { - const { - miniPromptEventType, - largePromptEventType, - dismissableEventType, - autoPromptType, - } = params; - it(`should not store a ${autoPromptType} impression if a previous prompt impression has been stored`, async () => { + it('should not store event timestamps for a non frequency capping event', async () => { autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - sandbox.match.any, - /* useLocalStorage */ true - ) - .resolves() - .exactly(1); - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSALS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSALS, - sandbox.match.any, - /* useLocalStorage */ true - ) - .resolves() - .once(); + storageMock.expects('get').never(); + storageMock.expects('set').never(); await eventManagerCallback({ - eventType: miniPromptEventType, + eventType: AnalyticsEvent.ACTION_TWG_CREATOR_BENEFIT_CLICK, eventOriginator: EventOriginator.UNKNOWN_CLIENT, isFromUserAction: null, additionalParameters: null, }); + }); + + it('should ignore irrelevant events', async () => { + autoPromptManager.isClosable_ = true; + storageMock.expects('get').never(); + storageMock.expects('set').never(); + await eventManagerCallback({ - eventType: largePromptEventType, + eventType: AnalyticsEvent.IMPRESSION_AD, eventOriginator: EventOriginator.UNKNOWN_CLIENT, isFromUserAction: null, additionalParameters: null, }); + }); + + it(`should properly prune and fill all local storage Timestamps`, async () => { + autoPromptManager.isClosable_ = true; + + expectFrequencyCappingTimestamps(storageMock, { + 'TYPE_CONTRIBUTION': {impressions: [CURRENT_TIME]}, + 'TYPE_NEWSLETTER_SIGNUP': { + impressions: [CURRENT_TIME - TWO_WEEKS_IN_MILLIS - 1], + dismissals: [CURRENT_TIME - TWO_WEEKS_IN_MILLIS - 1], + }, + // Unsupported case where action is in local storage with no timestamps + 'TYPE_REWARDED_SURVEY': {}, + 'TYPE_REWARDED_AD': { + impressions: [ + CURRENT_TIME - TWO_WEEKS_IN_MILLIS - 1, + CURRENT_TIME - TWO_WEEKS_IN_MILLIS, + ], + completions: [CURRENT_TIME], + }, + }); + const timestamps = await autoPromptManager.getTimestamps(); + expect(JSON.stringify(timestamps)).to.equal( + JSON.stringify({ + 'TYPE_CONTRIBUTION': { + impressions: [CURRENT_TIME], + dismissals: [], + completions: [], + }, + 'TYPE_NEWSLETTER_SIGNUP': { + impressions: [], + dismissals: [], + completions: [], + }, + 'TYPE_REWARDED_SURVEY': { + impressions: [], + dismissals: [], + completions: [], + }, + 'TYPE_REWARDED_AD': { + impressions: [CURRENT_TIME - TWO_WEEKS_IN_MILLIS], + dismissals: [], + completions: [CURRENT_TIME], + }, + }) + ); + }); + + // Impression Events + [ + {eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS}, + {eventType: AnalyticsEvent.IMPRESSION_NEWSLETTER_OPT_IN}, + {eventType: AnalyticsEvent.IMPRESSION_BYOP_NEWSLETTER_OPT_IN}, + {eventType: AnalyticsEvent.IMPRESSION_REGWALL_OPT_IN}, + {eventType: AnalyticsEvent.IMPRESSION_SURVEY}, + {eventType: AnalyticsEvent.IMPRESSION_REWARDED_AD}, + {eventType: AnalyticsEvent.IMPRESSION_OFFERS}, + ].forEach(({eventType}) => { + it(`should not store impression timestamps for event ${eventType} for nondismissible prompts`, async () => { + autoPromptManager.isClosable_ = false; + storageMock.expects('get').never(); + storageMock.expects('set').never(); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + }); + }); + + [ + { + eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.IMPRESSION_NEWSLETTER_OPT_IN, + action: 'TYPE_NEWSLETTER_SIGNUP', + }, + { + eventType: AnalyticsEvent.IMPRESSION_BYOP_NEWSLETTER_OPT_IN, + action: 'TYPE_NEWSLETTER_SIGNUP', + }, + { + eventType: AnalyticsEvent.IMPRESSION_REGWALL_OPT_IN, + action: 'TYPE_REGISTRATION_WALL', + }, + { + eventType: AnalyticsEvent.IMPRESSION_SURVEY, + action: 'TYPE_REWARDED_SURVEY', + }, + { + eventType: AnalyticsEvent.IMPRESSION_REWARDED_AD, + action: 'TYPE_REWARDED_AD', + }, + { + eventType: AnalyticsEvent.IMPRESSION_OFFERS, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, action}) => { + it(`for eventType=${eventType}, should set impression timestamps for action=${action}`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps(storageMock, '', { + [action]: {impressions: [CURRENT_TIME]}, + }); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + }); + }); + + it(`should set impression timestamps to existing localstorage`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps( + storageMock, + {}, + {'TYPE_REWARDED_SURVEY': {impressions: [CURRENT_TIME]}} + ); + await eventManagerCallback({ - eventType: dismissableEventType, + eventType: AnalyticsEvent.IMPRESSION_SURVEY, eventOriginator: EventOriginator.UNKNOWN_CLIENT, isFromUserAction: null, additionalParameters: null, }); }); - }); - [ - { - miniPromptEventType: - AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, - largePromptEventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - dismissableEventType: AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED, - autoPromptType: 'TYPE_CONTRIBUTION', - }, - { - miniPromptEventType: - AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT, - largePromptEventType: AnalyticsEvent.IMPRESSION_OFFERS, - dismissableEventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, - autoPromptType: 'TYPE_SUBSCRIPTION', - }, - ].forEach((params) => { - const { - miniPromptEventType, - largePromptEventType, - dismissableEventType, - autoPromptType, - } = params; - it(`should not store a frequency capping ${autoPromptType} impression if a previous prompt impression has been stored`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; + it(`should add impression event to existing localstorage with impression timestamps`, async () => { autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps(storageMock, '', { - [autoPromptType]: {impressions: [CURRENT_TIME]}, + expectFrequencyCappingTimestamps( + storageMock, + {'TYPE_REWARDED_SURVEY': {impressions: [CURRENT_TIME]}}, + { + 'TYPE_REWARDED_SURVEY': { + impressions: [CURRENT_TIME, CURRENT_TIME], + }, + } + ); + + await eventManagerCallback({ + eventType: AnalyticsEvent.IMPRESSION_SURVEY, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, }); + }); + + it(`should set impression timestamps for existing local storage timestamp but no impressions`, async () => { + autoPromptManager.isClosable_ = true; expectFrequencyCappingTimestamps( storageMock, - {[autoPromptType]: {impressions: [CURRENT_TIME]}}, + {'TYPE_REWARDED_SURVEY': {dismissals: [CURRENT_TIME]}}, { - [autoPromptType]: { - impressions: [CURRENT_TIME], + 'TYPE_REWARDED_SURVEY': { dismissals: [CURRENT_TIME], + impressions: [CURRENT_TIME], }, } ); - // Legacy storage operations + + await eventManagerCallback({ + eventType: AnalyticsEvent.IMPRESSION_SURVEY, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + }); + + it(`should set all event timestamps for a given prompt on storing impressions`, async () => { + autoPromptManager.isClosable_ = true; storageMock .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) + .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) + .resolves('') .once(); storageMock .expects('set') .withExactArgs( - StorageKeys.IMPRESSIONS, - sandbox.match.any, + StorageKeys.TIMESTAMPS, + JSON.stringify({ + 'TYPE_REWARDED_SURVEY': { + impressions: [CURRENT_TIME], + dismissals: [], + completions: [], + }, + }), /* useLocalStorage */ true ) - .resolves() - .exactly(1); - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSALS, /* useLocalStorage */ true) .resolves(null) .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSALS, - sandbox.match.any, - /* useLocalStorage */ true - ) - .resolves() - .once(); - await eventManagerCallback({ - eventType: miniPromptEventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, + await autoPromptManager.storeImpression('TYPE_REWARDED_SURVEY'); + }); + + // Dismissal Events + [ + {eventType: AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED}, + {eventType: AnalyticsEvent.ACTION_NEWSLETTER_OPT_IN_CLOSE}, + {eventType: AnalyticsEvent.ACTION_BYOP_NEWSLETTER_OPT_IN_CLOSE}, + {eventType: AnalyticsEvent.ACTION_REGWALL_OPT_IN_CLOSE}, + {eventType: AnalyticsEvent.ACTION_SURVEY_CLOSED}, + {eventType: AnalyticsEvent.ACTION_REWARDED_AD_CLOSE}, + {eventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED}, + ].forEach(({eventType}) => { + it(`should not store dismissal timestamps for event ${eventType} for nondismissible prompts`, async () => { + autoPromptManager.isClosable_ = false; + storageMock.expects('get').never(); + storageMock.expects('set').never(); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + }); + }); + + [ + { + eventType: AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.ACTION_NEWSLETTER_OPT_IN_CLOSE, + action: 'TYPE_NEWSLETTER_SIGNUP', + }, + { + eventType: AnalyticsEvent.ACTION_BYOP_NEWSLETTER_OPT_IN_CLOSE, + action: 'TYPE_NEWSLETTER_SIGNUP', + }, + { + eventType: AnalyticsEvent.ACTION_REGWALL_OPT_IN_CLOSE, + action: 'TYPE_REGISTRATION_WALL', + }, + { + eventType: AnalyticsEvent.ACTION_SURVEY_CLOSED, + action: 'TYPE_REWARDED_SURVEY', + }, + { + eventType: AnalyticsEvent.ACTION_REWARDED_AD_CLOSE, + action: 'TYPE_REWARDED_AD', + }, + { + eventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, action}) => { + it(`for eventType=${eventType}, should set dismissals via local storage for action=${action}`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps(storageMock, '', { + [action]: {dismissals: [CURRENT_TIME]}, + }); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); + }); + + it(`should add dismissals to existing local storage timestamps`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps( + storageMock, + {'TYPE_REWARDED_SURVEY': {dismissals: [CURRENT_TIME]}}, + { + 'TYPE_REWARDED_SURVEY': { + dismissals: [CURRENT_TIME, CURRENT_TIME], + }, + } + ); + await eventManagerCallback({ - eventType: largePromptEventType, + eventType: AnalyticsEvent.ACTION_SURVEY_CLOSED, eventOriginator: EventOriginator.UNKNOWN_CLIENT, isFromUserAction: null, additionalParameters: null, }); + }); + + it(`should add dismissals to existing local storage timestamp but no dismissals`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps( + storageMock, + {'TYPE_REWARDED_SURVEY': {impressions: [CURRENT_TIME]}}, + { + 'TYPE_REWARDED_SURVEY': { + impressions: [CURRENT_TIME], + dismissals: [CURRENT_TIME], + }, + } + ); + await eventManagerCallback({ - eventType: dismissableEventType, + eventType: AnalyticsEvent.ACTION_SURVEY_CLOSED, eventOriginator: EventOriginator.UNKNOWN_CLIENT, isFromUserAction: null, additionalParameters: null, }); }); - }); - it('should locally store contribution dismissals when contribution dismissal events are fired', async () => { - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSALS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSALS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLOSE, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); + it(`should set all event timestamps for a given prompt on storing dismissals`, async () => { + autoPromptManager.isClosable_ = true; + storageMock + .expects('get') + .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) + .resolves('') + .once(); + storageMock + .expects('set') + .withExactArgs( + StorageKeys.TIMESTAMPS, + JSON.stringify({ + 'TYPE_REWARDED_SURVEY': { + impressions: [], + dismissals: [CURRENT_TIME], + completions: [], + }, + }), + /* useLocalStorage */ true + ) + .resolves(null) + .once(); - it('should locally store subscription dismissals when subscription dismissal events are fired', async () => { - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSALS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSALS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SWG_SUBSCRIPTION_MINI_PROMPT_CLOSE, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, + await autoPromptManager.storeDismissal('TYPE_REWARDED_SURVEY'); }); - }); - - it('should record the last dismissed flow if one was setup', async () => { - autoPromptManager.interventionDisplayed_ = { - type: AutoPromptType.CONTRIBUTION, - }; - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSALS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSED_PROMPTS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSALS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSED_PROMPTS, - AutoPromptType.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLOSE, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, + // Completion Events + [ + {eventType: AnalyticsEvent.EVENT_CONTRIBUTION_PAYMENT_COMPLETE}, + {eventType: AnalyticsEvent.ACTION_NEWSLETTER_OPT_IN_BUTTON_CLICK}, + {eventType: AnalyticsEvent.ACTION_BYOP_NEWSLETTER_OPT_IN_SUBMIT}, + {eventType: AnalyticsEvent.ACTION_REGWALL_OPT_IN_BUTTON_CLICK}, + {eventType: AnalyticsEvent.ACTION_SURVEY_SUBMIT_CLICK}, + {eventType: AnalyticsEvent.ACTION_REWARDED_AD_VIEW}, + {eventType: AnalyticsEvent.EVENT_SUBSCRIPTION_PAYMENT_COMPLETE}, + ].forEach(({eventType}) => { + it(`should not store completion timestamps for event ${eventType} for nondismissible prompts`, async () => { + autoPromptManager.isClosable_ = false; + storageMock.expects('get').never(); + storageMock.expects('set').never(); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + }); }); - }); - it('should record survey completed on survey submit action', async () => { - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = false; - storageMock - .expects('set') - .withExactArgs( - StorageKeys.SURVEY_COMPLETED, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SURVEY_DATA_TRANSFER, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it(`should properly prune and fill all local storage Timestamps`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - - expectFrequencyCappingTimestamps(storageMock, { - 'TYPE_CONTRIBUTION': {impressions: [CURRENT_TIME]}, - 'TYPE_NEWSLETTER_SIGNUP': { - impressions: [CURRENT_TIME - TWO_WEEKS_IN_MILLIS - 1], - dismissals: [CURRENT_TIME - TWO_WEEKS_IN_MILLIS - 1], + [ + { + eventType: AnalyticsEvent.EVENT_CONTRIBUTION_PAYMENT_COMPLETE, + action: 'TYPE_CONTRIBUTION', }, - // Unsupported case where action is in local storage with no timestamps - 'TYPE_REWARDED_SURVEY': {}, - 'TYPE_REWARDED_AD': { - impressions: [ - CURRENT_TIME - TWO_WEEKS_IN_MILLIS - 1, - CURRENT_TIME - TWO_WEEKS_IN_MILLIS, - ], - completions: [CURRENT_TIME], + { + eventType: AnalyticsEvent.ACTION_NEWSLETTER_OPT_IN_BUTTON_CLICK, + action: 'TYPE_NEWSLETTER_SIGNUP', }, - }); - const timestamps = await autoPromptManager.getTimestamps(); - expect(JSON.stringify(timestamps)).to.equal( - JSON.stringify({ - 'TYPE_CONTRIBUTION': { - impressions: [CURRENT_TIME], - dismissals: [], - completions: [], - }, - 'TYPE_NEWSLETTER_SIGNUP': { - impressions: [], - dismissals: [], - completions: [], - }, - 'TYPE_REWARDED_SURVEY': { - impressions: [], - dismissals: [], - completions: [], - }, - 'TYPE_REWARDED_AD': { - impressions: [CURRENT_TIME - TWO_WEEKS_IN_MILLIS], - dismissals: [], - completions: [CURRENT_TIME], - }, - }) - ); - }); - - it(`should set all timestamps on storing impressions`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) - .resolves('') - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.TIMESTAMPS, - JSON.stringify({ - 'TYPE_REWARDED_SURVEY': { - impressions: [CURRENT_TIME], - dismissals: [], - completions: [], - }, - }), - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - - await autoPromptManager.storeImpression('TYPE_REWARDED_SURVEY'); - }); - - it(`should set all timestamps on storing dismissals`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) - .resolves('') - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.TIMESTAMPS, - JSON.stringify({ - 'TYPE_REWARDED_SURVEY': { - impressions: [], - dismissals: [CURRENT_TIME], - completions: [], - }, - }), - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - - await autoPromptManager.storeDismissal('TYPE_REWARDED_SURVEY'); - }); - - it(`should set all timestamps on storing completions`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) - .resolves('') - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.TIMESTAMPS, - JSON.stringify({ - 'TYPE_REWARDED_SURVEY': { - impressions: [], - dismissals: [], - completions: [CURRENT_TIME], - }, - }), - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - - await autoPromptManager.storeCompletion('TYPE_REWARDED_SURVEY'); - }); - - it('should not store events for a non frequency capping event', async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - - storageMock.expects('get').never(); - storageMock.expects('set').never(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_TWG_CREATOR_BENEFIT_CLICK, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should not store events when an impression or dismissal was fired for a paygated article', async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - sandbox.stub(pageConfig, 'isLocked').returns(true); - storageMock.expects('get').never(); - storageMock.expects('set').never(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should not store frequency capping events when an impression or dismissal was fired for a paygated article', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - storageMock.expects('get').never(); - storageMock.expects('set').never(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SURVEY_SUBMIT_CLICK, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should ignore undefined events', async () => { - storageMock.expects('get').never(); - storageMock.expects('set').never(); - - await eventManagerCallback({ - eventType: undefined, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should ignore irrelevant events', async () => { - storageMock.expects('get').never(); - storageMock.expects('set').never(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_AD, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should not set frequency cap local storage if experiment is disabled', async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = false; - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = true; - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .never(); - storageMock - .expects('set') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .never(); - // Legacy storage operations - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should not set frequency cap timestamps to local storage if experiment is disabled', async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = false; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = false; - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = true; - storageMock - .expects('get') - .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) - .never(); - // Legacy storage operations - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSALS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSALS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLOSE, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should not set frequency cap local storage if experiment is enabled and content is locked', async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = true; - sandbox.stub(pageConfig, 'isLocked').returns(true); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .never(); - storageMock - .expects('set') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .never(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should not set frequency cap local storage if experiment is enabled and prompt was nondismissible', async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = true; - autoPromptManager.isClosable_ = false; - sandbox.stub(pageConfig, 'isLocked').returns(true); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .never(); - storageMock - .expects('set') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .never(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it('should set frequency cap local storage if experiment is enabled and a dismissible contribution prompt was triggered on a locked page', async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.autoPromptType_ = AutoPromptType.CONTRIBUTION_LARGE; - autoPromptManager.isClosable_ = true; - sandbox.stub(pageConfig, 'isLocked').returns(true); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - - expect(autoPromptManager.hasStoredMiniPromptImpression_).to.equal(true); - }); - - it('should set frequency cap local storage if experiment is enabled and a monetization prompt was triggered', async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - // Legacy storage operations - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - - expect(autoPromptManager.hasStoredMiniPromptImpression_).to.equal(true); - }); - - it('should set frequency cap local storage if experiment is enabled and a mini monetization prompt was triggered', async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - // Legacy storage operations - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - - expect(autoPromptManager.hasStoredMiniPromptImpression_).to.equal(true); - }); - - it('should set frequency cap local storage only once if experiment is enabled and both mini and normal monetization prompts were triggered', async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - // Legacy storage operations - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - - expect(autoPromptManager.hasStoredMiniPromptImpression_).to.equal(true); - }); - - it('should not set frequency cap local storage if experiment is enabled and hasStoredImpression is true and a monetization prompt was triggered', async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - autoPromptManager.hasStoredMiniPromptImpression_ = true; - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .never(); - storageMock - .expects('set') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .never(); - // Legacy storage operations - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - [ - { - eventType: AnalyticsEvent.IMPRESSION_NEWSLETTER_OPT_IN, - storageKey: ImpressionStorageKeys.NEWSLETTER_SIGNUP, - }, - { - eventType: AnalyticsEvent.IMPRESSION_BYOP_NEWSLETTER_OPT_IN, - storageKey: ImpressionStorageKeys.NEWSLETTER_SIGNUP, - }, - { - eventType: AnalyticsEvent.IMPRESSION_REGWALL_OPT_IN, - storageKey: ImpressionStorageKeys.REGISTRATION_WALL, - }, - { - eventType: AnalyticsEvent.IMPRESSION_SURVEY, - storageKey: ImpressionStorageKeys.REWARDED_SURVEY, - }, - { - eventType: AnalyticsEvent.IMPRESSION_REWARDED_AD, - storageKey: ImpressionStorageKeys.REWARDED_AD, - }, - ].forEach(({eventType, storageKey}) => { - it(`for eventType=${eventType} and storageKey=${storageKey}, should set frequency cap timestamps via local storage if experiment is enabled`, async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs(storageKey, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - storageKey, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); + { + eventType: AnalyticsEvent.ACTION_BYOP_NEWSLETTER_OPT_IN_SUBMIT, + action: 'TYPE_NEWSLETTER_SIGNUP', + }, + { + eventType: AnalyticsEvent.ACTION_REGWALL_OPT_IN_BUTTON_CLICK, + action: 'TYPE_REGISTRATION_WALL', + }, + { + eventType: AnalyticsEvent.ACTION_SURVEY_SUBMIT_CLICK, + action: 'TYPE_REWARDED_SURVEY', + }, + { + eventType: AnalyticsEvent.ACTION_REWARDED_AD_VIEW, + action: 'TYPE_REWARDED_AD', + }, + { + eventType: AnalyticsEvent.EVENT_SUBSCRIPTION_PAYMENT_COMPLETE, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, action}) => { + it(`for eventType=${eventType}, should set completions via local storage for action=${action}`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps(storageMock, '', { + [action]: {completions: [CURRENT_TIME]}, + }); - await eventManagerCallback({ - eventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); }); - }); - [ - { - eventType: AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT, - action: 'TYPE_SUBSCRIPTION', - }, - {eventType: AnalyticsEvent.IMPRESSION_OFFERS, action: 'TYPE_SUBSCRIPTION'}, - ].forEach(({eventType, action}) => { - it(`for autoprompt eventType=${eventType}, should set frequency cap impressions via local storage for action=${action}`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; + it(`should add completions to existing local storage timestamps`, async () => { autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps(storageMock, '', { - [action]: {impressions: [CURRENT_TIME]}, - }); - // Legacy storage operations - // TODO(justinchou): once deprecated, join with other impression tests. - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); + expectFrequencyCappingTimestamps( + storageMock, + {'TYPE_REWARDED_SURVEY': {completions: [CURRENT_TIME]}}, + { + 'TYPE_REWARDED_SURVEY': { + completions: [CURRENT_TIME, CURRENT_TIME], + }, + } + ); await eventManagerCallback({ - eventType, + eventType: AnalyticsEvent.ACTION_SURVEY_SUBMIT_CLICK, eventOriginator: EventOriginator.UNKNOWN_CLIENT, isFromUserAction: null, additionalParameters: null, }); }); - }); - [ - { - eventType: AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT, - action: 'TYPE_SUBSCRIPTION', - }, - {eventType: AnalyticsEvent.IMPRESSION_OFFERS, action: 'TYPE_SUBSCRIPTION'}, - ].forEach(({eventType, action}) => { - it(`for autoprompt eventType=${eventType} and promptIsFromCta_ = true, should not set frequency cap impressions via local storage for action=${action}`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.promptIsFromCtaButton_ = true; + it(`should add completions to existing local storage timestamps but no completions`, async () => { autoPromptManager.isClosable_ = true; - storageMock - .expects('get') - .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) - .never(); - storageMock - .expects('set') - .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) - .never(); - // Legacy storage operations - // TODO(justinchou): once deprecated, join with other impression tests. - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.IMPRESSIONS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); + expectFrequencyCappingTimestamps( + storageMock, + {'TYPE_REWARDED_SURVEY': {impressions: [CURRENT_TIME]}}, + { + 'TYPE_REWARDED_SURVEY': { + impressions: [CURRENT_TIME], + completions: [CURRENT_TIME], + }, + } + ); await eventManagerCallback({ - eventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - }); - - [ - { - eventType: AnalyticsEvent.IMPRESSION_NEWSLETTER_OPT_IN, - action: 'TYPE_NEWSLETTER_SIGNUP', - }, - { - eventType: AnalyticsEvent.IMPRESSION_BYOP_NEWSLETTER_OPT_IN, - action: 'TYPE_NEWSLETTER_SIGNUP', - }, - { - eventType: AnalyticsEvent.IMPRESSION_REGWALL_OPT_IN, - action: 'TYPE_REGISTRATION_WALL', - }, - { - eventType: AnalyticsEvent.IMPRESSION_SURVEY, - action: 'TYPE_REWARDED_SURVEY', - }, - { - eventType: AnalyticsEvent.IMPRESSION_REWARDED_AD, - action: 'TYPE_REWARDED_AD', - }, - ].forEach(({eventType, action}) => { - it(`for eventType=${eventType}, should set frequency cap impressions via local storage for action=${action}`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps(storageMock, '', { - [action]: {impressions: [CURRENT_TIME]}, - }); - - await eventManagerCallback({ - eventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - }); - - it(`should add frequency cap impressions to existing local storage timestamps`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps( - storageMock, - {'TYPE_REWARDED_SURVEY': {impressions: [CURRENT_TIME]}}, - { - 'TYPE_REWARDED_SURVEY': { - impressions: [CURRENT_TIME, CURRENT_TIME], - }, - } - ); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_SURVEY, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it(`should add frequency cap impressions to existing local storage timestamp but no impressions`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps( - storageMock, - {'TYPE_REWARDED_SURVEY': {dismissals: [CURRENT_TIME]}}, - { - 'TYPE_REWARDED_SURVEY': { - dismissals: [CURRENT_TIME], - impressions: [CURRENT_TIME], - }, - } - ); - - await eventManagerCallback({ - eventType: AnalyticsEvent.IMPRESSION_SURVEY, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - [ - { - eventType: AnalyticsEvent.ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLOSE, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.ACTION_SWG_SUBSCRIPTION_MINI_PROMPT_CLOSE, - action: 'TYPE_SUBSCRIPTION', - }, - { - eventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, - action: 'TYPE_SUBSCRIPTION', - }, - ].forEach(({eventType, action}) => { - it(`for autoprompt eventType=${eventType}, should set frequency cap dismissals via local storage for action=${action}`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps(storageMock, '', { - [action]: {dismissals: [CURRENT_TIME]}, - }); - // Legacy storage operations - // TODO(justinchou): once deprecated, join with other impression tests. - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSALS, /* useLocalStorage */ true) - .resolves(null) - .once(); - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSALS, - CURRENT_TIME.toString(), - /* useLocalStorage */ true - ) - .resolves() - .once(); - - await eventManagerCallback({ - eventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - }); - - [ - { - eventType: AnalyticsEvent.ACTION_NEWSLETTER_OPT_IN_CLOSE, - action: 'TYPE_NEWSLETTER_SIGNUP', - }, - { - eventType: AnalyticsEvent.ACTION_BYOP_NEWSLETTER_OPT_IN_CLOSE, - action: 'TYPE_NEWSLETTER_SIGNUP', - }, - { - eventType: AnalyticsEvent.ACTION_REGWALL_OPT_IN_CLOSE, - action: 'TYPE_REGISTRATION_WALL', - }, - { - eventType: AnalyticsEvent.ACTION_SURVEY_CLOSED, - action: 'TYPE_REWARDED_SURVEY', - }, - { - eventType: AnalyticsEvent.ACTION_REWARDED_AD_CLOSE, - action: 'TYPE_REWARDED_AD', - }, - ].forEach(({eventType, action}) => { - it(`for eventType=${eventType}, should set frequency cap dismissals via local storage for action=${action}`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps(storageMock, '', { - [action]: {dismissals: [CURRENT_TIME]}, - }); - - await eventManagerCallback({ - eventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - }); - - it(`should add frequency cap dismissals to existing local storage timestamps`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps( - storageMock, - {'TYPE_REWARDED_SURVEY': {dismissals: [CURRENT_TIME]}}, - { - 'TYPE_REWARDED_SURVEY': { - dismissals: [CURRENT_TIME, CURRENT_TIME], - }, - } - ); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SURVEY_CLOSED, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it(`should add frequency cap dismissals to existing local storage timestamp but no dismissals`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps( - storageMock, - {'TYPE_REWARDED_SURVEY': {impressions: [CURRENT_TIME]}}, - { - 'TYPE_REWARDED_SURVEY': { - impressions: [CURRENT_TIME], - dismissals: [CURRENT_TIME], - }, - } - ); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SURVEY_CLOSED, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - [ - { - eventType: AnalyticsEvent.EVENT_CONTRIBUTION_PAYMENT_COMPLETE, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.ACTION_NEWSLETTER_OPT_IN_BUTTON_CLICK, - action: 'TYPE_NEWSLETTER_SIGNUP', - }, - { - eventType: AnalyticsEvent.ACTION_BYOP_NEWSLETTER_OPT_IN_SUBMIT, - action: 'TYPE_NEWSLETTER_SIGNUP', - }, - { - eventType: AnalyticsEvent.ACTION_REGWALL_OPT_IN_BUTTON_CLICK, - action: 'TYPE_REGISTRATION_WALL', - }, - { - eventType: AnalyticsEvent.ACTION_SURVEY_SUBMIT_CLICK, - action: 'TYPE_REWARDED_SURVEY', - }, - { - eventType: AnalyticsEvent.ACTION_REWARDED_AD_VIEW, - action: 'TYPE_REWARDED_AD', - }, - { - eventType: AnalyticsEvent.EVENT_SUBSCRIPTION_PAYMENT_COMPLETE, - action: 'TYPE_SUBSCRIPTION', - }, - ].forEach(({eventType, action}) => { - it(`for eventType=${eventType}, should set frequency cap completions via local storage for action=${action}`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps(storageMock, '', { - [action]: {completions: [CURRENT_TIME]}, - }); - - await eventManagerCallback({ - eventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - }); - - it(`should add frequency cap completions to existing local storage timestamps`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps( - storageMock, - {'TYPE_REWARDED_SURVEY': {completions: [CURRENT_TIME]}}, - { - 'TYPE_REWARDED_SURVEY': { - completions: [CURRENT_TIME, CURRENT_TIME], - }, - } - ); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SURVEY_SUBMIT_CLICK, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - it(`should add frequency cap completions to existing local storage timestamps but no completions`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - expectFrequencyCappingTimestamps( - storageMock, - {'TYPE_REWARDED_SURVEY': {impressions: [CURRENT_TIME]}}, - { - 'TYPE_REWARDED_SURVEY': { - impressions: [CURRENT_TIME], - completions: [CURRENT_TIME], - }, - } - ); - - await eventManagerCallback({ - eventType: AnalyticsEvent.ACTION_SURVEY_SUBMIT_CLICK, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - - [ - { - eventType: AnalyticsEvent.EVENT_PAYMENT_FAILED, - autoPromptType: AutoPromptType.CONTRIBUTION, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.EVENT_PAYMENT_FAILED, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - action: 'TYPE_CONTRIBUTION', - }, - { - eventType: AnalyticsEvent.EVENT_PAYMENT_FAILED, - autoPromptType: AutoPromptType.SUBSCRIPTION, - action: 'TYPE_SUBSCRIPTION', - }, - { - eventType: AnalyticsEvent.EVENT_PAYMENT_FAILED, - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - action: 'TYPE_SUBSCRIPTION', - }, - ].forEach(({eventType, autoPromptType, action}) => { - it(`for generic eventType=${eventType}, should set frequency cap completions via local storage for autoPromptType=${autoPromptType}`, async () => { - autoPromptManager.frequencyCappingByDismissalsEnabled_ = true; - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - autoPromptManager.isClosable_ = true; - autoPromptManager.autoPromptType_ = autoPromptType; - expectFrequencyCappingTimestamps(storageMock, '', { - [action]: {completions: [CURRENT_TIME]}, - }); - - await eventManagerCallback({ - eventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - }); - }); - - [ - { - eventType: AnalyticsEvent.ACTION_SWG_BUTTON_SHOW_OFFERS_CLICK, - }, - { - eventType: AnalyticsEvent.ACTION_SWG_BUTTON_SHOW_CONTRIBUTIONS_CLICK, - }, - ].forEach(({eventType}) => { - it(`should set promptIsFromCtaButton on cta button action: ${eventType}`, async () => { - autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; - await eventManagerCallback({ - eventType, - eventOriginator: EventOriginator.UNKNOWN_CLIENT, - isFromUserAction: null, - additionalParameters: null, - }); - expect(autoPromptManager.promptIsFromCtaButton_).to.be.true; - }); - }); - - it('should display the mini prompt, but not fetch entitlements and client config if alwaysShow is enabled', async () => { - entitlementsManagerMock.expects('getEntitlements').never(); - clientConfigManagerMock.expects('getAutoPromptConfig').never(); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: true, - }); - }); - - it('should display the large prompt, but not fetch entitlements and client config if alwaysShow is enabled', async () => { - entitlementsManagerMock.expects('getEntitlements').never(); - clientConfigManagerMock.expects('getAutoPromptConfig').never(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - alwaysShow: true, - }); - }); - - it('should not display a prompt if the autoprompttype is unknown and alwaysShow is enabled', async () => { - entitlementsManagerMock.expects('getEntitlements').never(); - clientConfigManagerMock.expects('getAutoPromptConfig').never(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: 'UNKNOWN', - alwaysShow: true, - }); - }); - - it('should not show monetization prompt as soft paywall if the type is undefined', async () => { - const shouldShow = - await autoPromptManager.shouldShowMonetizationPromptAsSoftPaywall( - undefined, - false - ); - expect(shouldShow).to.be.false; - }); - - it('should not show monetization prompt as soft paywall if the type is NONE', async () => { - const shouldShow = - await autoPromptManager.shouldShowMonetizationPromptAsSoftPaywall( - AutoPromptType.NONE, - false - ); - expect(shouldShow).to.be.false; - }); - - it('should not display a prompt if the type is undefined', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const clientConfig = new ClientConfig({}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: undefined, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - expect(subscriptionPromptFnSpy).to.not.be.called; - }); - - it('should not display a prompt if the type is NONE', async () => { - entitlementsManagerMock.expects('getEntitlements').never(); - entitlementsManagerMock.expects('getArticle').never(); - clientConfigManagerMock.expects('getClientConfig').never(); - storageMock.expects('get').never(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.NONE, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(autoPromptManager.getLastAudienceActionFlow()).to.equal(null); - expect(contributionPromptFnSpy).to.not.be.called; - expect(subscriptionPromptFnSpy).to.not.be.called; - }); - - it('should not display any prompt if the auto prompt config is not returned', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .returns(clientConfig) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should not display any prompt if canDisplayAutoPrompt is false', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ false); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .returns(clientConfig) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - expect(subscriptionPromptFnSpy).to.not.be.called; - }); - - it('should display the mini prompt if the user has no entitlements and auto prompt config does not cap impressions', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should not display the mini contribution prompt if the article is null', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock.expects('getArticle').resolves(null).once(); - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should not display the mini contribution prompt if the article returns no actions', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock.expects('getArticle').resolves({}).once(); - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should not display the mini prompt if the auto prompt config caps impressions, and the user is over the cap, and sufficient time has not yet passed since the specified hide duration', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // Two stored impressions. - const storedImpressions = - (CURRENT_TIME - 1).toString() + ',' + CURRENT_TIME.toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should display the mini prompt if the auto prompt config caps impressions, and the user is over the cap, but sufficient time has passed since the specified hide duration', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // Two stored impressions. - const storedImpressions = - (CURRENT_TIME - 20000).toString() + - ',' + - (CURRENT_TIME - 11000).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should display the mini prompt if the auto prompt config caps impressions, and the user is under the cap', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // One stored impression. - const storedImpressions = CURRENT_TIME.toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should not display the mini prompt if the auto prompt config caps impressions, and the user is under the cap, but sufficient time has not yet passed since the specified backoff duration', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - impressionBackOffSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 5, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // One stored impression. - const storedImpressions = (CURRENT_TIME - 6000).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should display the large prompt if the auto prompt config caps impressions, and the user is under the cap', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.be.calledOnce; - }); - - it('should display the mini prompt if the auto prompt config caps impressions, and the user is under the cap after discounting old impressions', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // Two stored impressions, the first from 2 weeks ago. - const storedImpressions = - (CURRENT_TIME - TWO_WEEKS_IN_MILLIS).toString() + - ',' + - CURRENT_TIME.toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should not display the mini prompt if the auto prompt config caps dismissals, and the user is over the cap', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 0, - maxDismissalsPerWeek: 1, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // One stored impression from 10ms ago and one dismissal from 5ms ago. - const storedImpressions = (CURRENT_TIME - 10).toString(); - const storedDismissals = (CURRENT_TIME - 5).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - storedDismissals, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should display the mini prompt if the auto prompt config caps dismissals, and the user is over the cap, but sufficient time has passed since the specified hide duration', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 0, - maxDismissalsPerWeek: 1, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // One stored impression from 20s ago and one dismissal from 11s ago. - const storedImpressions = (CURRENT_TIME - 20000).toString(); - const storedDismissals = (CURRENT_TIME - 11000).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - storedDismissals, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('handles null values for maxDismissalsResultingHideSeconds and maxImpressionsResultingHideSeconds', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - maxDismissalsResultingHideSeconds: null, - maxImpressionsResultingHideSeconds: null, - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 0, - maxDismissalsPerWeek: 1, - maxImpressions: 2, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // One stored impression from 20s ago and one dismissal from 11s ago. - const storedImpressions = (CURRENT_TIME - 20000).toString(); - const storedDismissals = (CURRENT_TIME - 11000).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - storedDismissals, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should not display the mini prompt if the auto prompt config caps dismissals, and the user is under the cap, but sufficient time has not yet passed since the backoff duration', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 10, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 5, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // One stored impression from 20s ago and one dismissal from 6s ago. - const storedImpressions = (CURRENT_TIME - 20000).toString(); - const storedDismissals = (CURRENT_TIME - 6000).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - storedDismissals, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should display the mini prompt if the auto prompt config caps dismissals, and the user is under the cap, and sufficient time has passed since the specified backoff duration', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - // One stored impression from 20s ago and one dismissal from 6s ago. - const storedImpressions = (CURRENT_TIME - 20000).toString(); - const storedDismissals = (CURRENT_TIME - 6000).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - storedDismissals, - dismissedPromptGetCallCount: 1, - getUserToken: false, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should display the subscription mini prompt if the user has no entitlements', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [SUBSCRIPTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.SUBSCRIPTION, - alwaysShow: false, - }); - await tick(10); - - expect(subscriptionPromptFnSpy).to.not.be.called; - }); - - it('should not display the mini subscription prompt if the article returns no actions', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock.expects('getArticle').resolves({}).once(); - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.SUBSCRIPTION, - alwaysShow: false, - }); - await tick(10); - - expect(subscriptionPromptFnSpy).to.not.be.called; - }); - - [ - { - autoPromptType: AutoPromptType.SUBSCRIPTION, - interventionDisplayed: 'TYPE_SUBSCRIPTION', - }, - { - autoPromptType: AutoPromptType.CONTRIBUTION, - interventionDisplayed: 'TYPE_CONTRIBUTION', - }, - {autoPromptType: 'UNKNOWN', interventionDisplayed: undefined}, - ].forEach(({autoPromptType, interventionDisplayed}) => { - it(`should set autoPromptManager internal state for autoPromptType: (${autoPromptType})`, async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [ - { - type: interventionDisplayed, - configurationId: 'config_id', - }, - ], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType, - alwaysShow: false, - }); - await tick(10); - - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(autoPromptManager.interventionDisplayed_?.type).to.equal( - interventionDisplayed - ); - }); - }); - - it('should not display any prompt if the user has a valid entitlement', async () => { - const entitlements = new Entitlements(); - sandbox.stub(entitlements, 'enablesThis').returns(true); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should display a blocking prompt for locked content in the contribution flow if the user has no entitlements', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({autoPromptConfig, uiPredicates}); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.be.calledOnce; - }); - - [ - { - autoPromptType: AutoPromptType.CONTRIBUTION, - }, - { - autoPromptType: AutoPromptType.SUBSCRIPTION, - }, - ].forEach(({autoPromptType}) => { - it(`should not display any monetization prompt if the article returns no actions for autoPromptType: ${autoPromptType}`, async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - const entitlements = new Entitlements(); - sandbox.stub(entitlements, 'enablesThis').returns(false); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [ - // No action is eligible - ], - engineId: '123', - }, - }) - .once(); - - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - useUpdatedOfferFlows: true, - uiPredicates, - }); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(contributionPromptFnSpy).to.not.be.called; - expect(subscriptionPromptFnSpy).to.not.be.called; - }); - }); - - it('should display the contribution mini prompt if the user has no entitlements and UI predicate is true', async () => { - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - entitlementsManagerMock - .expects('getArticle') - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - - const autoPromptConfig = new AutoPromptConfig({}); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - useUpdatedOfferFlows: true, - uiPredicates, - }); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.not.be.called; - }); - - it('should log events when a large prompt overrides the miniprompt', async () => { - win./*OK*/ innerWidth = 500; - setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); - const expectedEvent = { - eventType: AnalyticsEvent.EVENT_DISABLE_MINIPROMPT_DESKTOP, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: { - publicationid: pubId, - promptType: AutoPromptType.CONTRIBUTION, - }, - timestamp: sandbox.match.number, - }; - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: true, - }); - await tick(10); - - expect(logEventSpy).to.be.calledOnceWith(expectedEvent); - }); - - it('should replace the contribution miniprompt with a large prompt if DISABLE_DESKTOP_MINIPROMPT is enabled and viewport is wider than 480px', async () => { - win./*OK*/ innerWidth = 500; - setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: true, - }); - await tick(10); - - expect(contributionPromptFnSpy).to.be.calledOnce; - }); - - it('should replace the subscription miniprompt with a large prompt if DISABLE_DESKTOP_MINIPROMPT is enabled and viewport is wider than 480px', async () => { - win./*OK*/ innerWidth = 500; - setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.SUBSCRIPTION, - alwaysShow: true, - }); - await tick(10); - - expect(subscriptionPromptFnSpy).to.be.calledOnce; - }); - - it('should not replace the miniprompt with a large prompt when DISABLE_DESKTOP_MINIPROMPT is enabled but the viewport is narrower than 480px', async () => { - win./*OK*/ innerWidth = 450; - setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); - const expectedEvent = { - eventType: AnalyticsEvent.EVENT_DISABLE_MINIPROMPT_DESKTOP, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: { - publicationid: pubId, - promptType: AutoPromptType.CONTRIBUTION, - }, - }; - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: true, - }); - await tick(10); - - logEventSpy.should.not.have.been.calledWith(expectedEvent); - expect(contributionPromptFnSpy).to.not.be.called; - }); - - describe('AudienceActionFlow', () => { - let getArticleExpectation; - - beforeEach(() => { - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = false; - const entitlements = new Entitlements(); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - const autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - useUpdatedOfferFlows: true, - uiPredicates, - }); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - getArticleExpectation = entitlementsManagerMock.expects('getArticle'); - getArticleExpectation - .resolves({ - audienceActions: { - actions: [REGWALL_INTERVENTION, SUBSCRIPTION_INTERVENTION], - engineId: '123', - }, - }) - .once(); - }); - - it('should set article experiment flag from experiment config', async () => { - getArticleExpectation.resolves({ - experimentConfig: { - experimentFlags: [ - 'fcbd_exp', - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, - }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - - expect(autoPromptManager.frequencyCappingByDismissalsEnabled_).to.equal( - true - ); - expect(autoPromptManager.frequencyCappingLocalStorageEnabled_).to.equal( - true - ); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - }); - - it('should display an AudienceActionFlow if the page is locked and there are any actions provided in the article response', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - alwaysShow: false, - }); - await tick(7); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - isClosable: false, - }); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.getLastAudienceActionFlow()).to.not.equal(null); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - }); - - it('should override isClosable if page is locked and isClosable is defined', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - alwaysShow: false, - isClosable: true, - }); - await tick(7); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - isClosable: true, - }); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.getLastAudienceActionFlow()).to.not.equal(null); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - }); - - it('should show the first Contribution prompt for the contribution flow', async () => { - getArticleExpectation - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION, NEWSLETTER_INTERVENTION], - engineId: '123', - }, - }) - .once(); - - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(contributionPromptFnSpy).to.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(autoPromptManager.getLastAudienceActionFlow()).to.equal(null); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_CONTRIBUTION' - ); - }); - - it('should show a soft paywall for unlocked content in the subscription flow', async () => { - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(subscriptionPromptFnSpy).to.have.been.calledOnce; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(autoPromptManager.getLastAudienceActionFlow()).to.equal(null); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_SUBSCRIPTION' - ); - }); - - // Note: Locked content on contribution is not an officially supported flow - it('should show an uncapped contribution prompt for locked content in the contribution flow', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - getArticleExpectation - .resolves({ - audienceActions: { - actions: [CONTRIBUTION_INTERVENTION, SURVEY_INTERVENTION], - engineId: '123', - }, - }) - .once(); - - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(contributionPromptFnSpy).to.have.been.calledOnce; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.getLastAudienceActionFlow()).to.equal(null); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_CONTRIBUTION' - ); - }); - - it('should show an uncapped prompt for paywalled content in the subscription flow', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - isClosable: false, - }); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.getLastAudienceActionFlow()).to.not.equal(null); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - }); - - [ - { - autoPromptType: AutoPromptType.CONTRIBUTION, - }, - { - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - }, - { - autoPromptType: AutoPromptType.SUBSCRIPTION, - }, - { - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - }, - ].forEach(({autoPromptType}) => { - it(`should not show any prompt if the article returns no actions for autoPromptType: ${autoPromptType}`, async () => { - getArticleExpectation - .resolves({ - audienceActions: {}, - }) - .once(); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType, - alwaysShow: false, - }); - await tick(8); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(subscriptionPromptFnSpy).to.not.have.been.called; - }); - }); - - it('should return the last AudienceActionFlow', async () => { - const lastAudienceActionFlow = - new audienceActionFlow.AudienceActionIframeFlow(deps, { - action: 'TYPE_REGISTRATION_WALL', - onCancel: undefined, - autoPromptType: AutoPromptType.CONTRIBUTION, - }); - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - autoPromptManager.setLastAudienceActionFlow(lastAudienceActionFlow); - - expect(autoPromptManager.getLastAudienceActionFlow()).to.equal( - lastAudienceActionFlow - ); - }); - }); - - describe('Contribution Flows with Audience Actions', () => { - let getArticleExpectation; - - beforeEach(() => { - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = false; - const autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates( - /* canDisplayAutoPrompt */ true, - /* canDisplayButton */ true - ); - const clientConfig = new ClientConfig({ - autoPromptConfig, - useUpdatedOfferFlows: true, - uiPredicates, - }); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - sandbox.stub(pageConfig, 'isLocked').returns(false); - const entitlements = new Entitlements(); - sandbox.stub(entitlements, 'enablesThis').returns(false); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - getArticleExpectation = entitlementsManagerMock.expects('getArticle'); - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - { - type: 'TYPE_CONTRIBUTION', - configurationId: 'contribution_config_id', - }, - SURVEY_INTERVENTION, - REGWALL_INTERVENTION, - NEWSLETTER_INTERVENTION, - ], - engineId: '123', - }, - }) - .once(); - }); - - it('should show the Contribution prompt before any actions', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutoPromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_CONTRIBUTION' - ); - }); - - it('should show the first Audience Action flow if a Contribution was previously dismissed and is not the next Contribution prompt time', async () => { - const storedImpressions = (CURRENT_TIME - 5).toString(); - const storedDismissals = (CURRENT_TIME - 10).toString(); - setupPreviousImpressionAndDismissals(storageMock, { - storedImpressions, - storedDismissals, - dismissedPrompts: AutoPromptType.CONTRIBUTION, - dismissedPromptGetCallCount: 2, - getUserToken: true, - }); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION, - isClosable: true, - }); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REWARDED_SURVEY' - ); - expect(autoPromptManager.interventionDisplayed_.configurationId).to.equal( - 'survey_config_id' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'contribution,TYPE_REWARDED_SURVEY' - ); - }); - - it('should show the second Audience Action flow if the first was previously dismissed and is not the next Contribution prompt time', async () => { - const storedImpressions = (CURRENT_TIME - 5).toString(); - const storedDismissals = (CURRENT_TIME - 10).toString(); - setupPreviousImpressionAndDismissals(storageMock, { - storedImpressions, - storedDismissals, - dismissedPrompts: 'contribution,TYPE_REWARDED_SURVEY', - dismissedPromptGetCallCount: 2, - getUserToken: true, - }); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION, - isClosable: true, - }); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'contribution,TYPE_REWARDED_SURVEY,TYPE_REGISTRATION_WALL' - ); - }); - - it('should show the third Audience Action flow if the first two were previously dismissed and is not the next Contribution prompt time', async () => { - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - CONTRIBUTION_INTERVENTION, - SURVEY_INTERVENTION, - REGWALL_INTERVENTION, - NEWSLETTER_INTERVENTION, - ], - engineId: '123', - }, - }) - .once(); - const storedImpressions = (CURRENT_TIME - 5).toString(); - const storedDismissals = (CURRENT_TIME - 10).toString(); - setupPreviousImpressionAndDismissals(storageMock, { - storedImpressions, - storedDismissals, - dismissedPrompts: 'contribution,TYPE_REWARDED_SURVEY', - dismissedPromptGetCallCount: 2, - getUserToken: true, - }); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION, - isClosable: true, - }); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'contribution,TYPE_REWARDED_SURVEY,TYPE_REGISTRATION_WALL' - ); - }); - - it('should skip survey and show second Audience Action flow if survey was completed', async () => { - const storedImpressions = (CURRENT_TIME - 5).toString(); - const storedDismissals = (CURRENT_TIME - 10).toString(); - const storedSurveyCompleted = (CURRENT_TIME - 5).toString(); - setupPreviousImpressionAndDismissals(storageMock, { - storedImpressions, - storedDismissals, - dismissedPrompts: AutoPromptType.CONTRIBUTION, - dismissedPromptGetCallCount: 2, - storedSurveyCompleted, - getUserToken: true, - }); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION, - isClosable: true, - }); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'contribution,TYPE_REGISTRATION_WALL' - ); - }); - - it('should skip survey and show second Audience Action flow if survey data transfer failed', async () => { - const storedImpressions = (CURRENT_TIME - 5).toString(); - const storedDismissals = (CURRENT_TIME - 10).toString(); - const storedSurveyFailed = (CURRENT_TIME - 5).toString(); - setupPreviousImpressionAndDismissals(storageMock, { - storedImpressions, - storedDismissals, - dismissedPrompts: AutoPromptType.CONTRIBUTION, - dismissedPromptGetCallCount: 2, - storedSurveyFailed, - getUserToken: true, - }); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION, - isClosable: true, - }); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'contribution,TYPE_REGISTRATION_WALL' - ); - }); - - it('should show nothing if the the last Audience Action was previously dismissed and is not in the next Contribution prompt time', async () => { - const storedImpressions = (CURRENT_TIME - 5).toString(); - const storedDismissals = (CURRENT_TIME - 10).toString(); - setupPreviousImpressionAndDismissals(storageMock, { - storedImpressions, - storedDismissals, - dismissedPrompts: - 'contribution,TYPE_REWARDED_SURVEY,TYPE_REGISTRATION_WALL,TYPE_NEWSLETTER_SIGNUP', - dismissedPromptGetCallCount: 1, - getUserToken: true, - }); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_).to.equal(null); - }); - - it('should show the Contribution Flow even if there is an available Audience Action that was previously dismissed and is in the next Contribution prompt time', async () => { - // One stored impression from 20s ago and one dismissal from 6s ago. - const storedImpressions = (CURRENT_TIME - 20000).toString(); - const storedDismissals = (CURRENT_TIME - 6000).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - storedDismissals, - dismissedPrompts: 'contribution,TYPE_REWARDED_SURVEY', - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutoPromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_CONTRIBUTION' - ); - }); - - [ - { - gaEligible: false, - gtagEligible: true, - }, - { - gaEligible: true, - gtagEligible: false, - }, - ].forEach(({gaEligible, gtagEligible}) => { - it(`should show survey if TYPE_REWARDED_SURVEY is next and is ga eligible: ${gaEligible}, is gTag eligible: ${gtagEligible}`, async () => { - setWinWithAnalytics(/* gtag */ gtagEligible, /* ga */ gaEligible); - const storedImpressions = (CURRENT_TIME - 5).toString(); - const storedDismissals = (CURRENT_TIME - 10).toString(); - setupPreviousImpressionAndDismissals(storageMock, { - storedImpressions, - storedDismissals, - dismissedPrompts: AutoPromptType.CONTRIBUTION, - dismissedPromptGetCallCount: 2, - getUserToken: true, - }); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION, - isClosable: true, - }); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REWARDED_SURVEY' - ); - expect( - autoPromptManager.interventionDisplayed_.configurationId - ).to.equal('survey_config_id'); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'contribution,TYPE_REWARDED_SURVEY' - ); - }); - }); - - it('should skip action and continue the Contribution Flow if TYPE_REWARDED_SURVEY is next but publisher is not eligible for ga nor gTag', async () => { - setWinWithAnalytics(/* gtag */ false, /* ga */ false); - const storedImpressions = (CURRENT_TIME - 5).toString(); - const storedDismissals = (CURRENT_TIME - 10).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - storedImpressions, - storedDismissals, - dismissedPrompts: AutoPromptType.CONTRIBUTION, - dismissedPromptGetCallCount: 2, - getUserToken: true, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION, - isClosable: true, - }); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'contribution,TYPE_REGISTRATION_WALL' - ); - }); - }); - - describe('Non-Monetary Revenue Model with Audience Actions', () => { - let getArticleExpectation; - - beforeEach(() => { - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = false; - const autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates( - /* canDisplayAutoPrompt */ true, - /* canDisplayButton */ true - ); - const clientConfig = new ClientConfig({ - autoPromptConfig, - useUpdatedOfferFlows: true, - uiPredicates, - }); - clientConfigManagerMock - .expects('getClientConfig') - .resolves(clientConfig) - .once(); - sandbox.stub(pageConfig, 'isLocked').returns(false); - const entitlements = new Entitlements(); - sandbox.stub(entitlements, 'enablesThis').returns(false); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - getArticleExpectation = entitlementsManagerMock.expects('getArticle'); - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - SURVEY_INTERVENTION, - REGWALL_INTERVENTION, - NEWSLETTER_INTERVENTION, - ], - engineId: '123', - }, - }) - .once(); - }); - - it('should show the first Audience Action flow', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 2, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: undefined, - isClosable: true, - }); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REWARDED_SURVEY' - ); - expect(autoPromptManager.interventionDisplayed_.configurationId).to.equal( - 'survey_config_id' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'TYPE_REWARDED_SURVEY' - ); - }); - - it('should show the second Audience Action flow if the first was previously dismissed', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPrompts: 'TYPE_REWARDED_SURVEY', - dismissedPromptGetCallCount: 2, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: undefined, - isClosable: true, - }); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'TYPE_REWARDED_SURVEY,TYPE_REGISTRATION_WALL' - ); - }); - - it('should show the third Audience Action flow if the first two were previously dismissed', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPrompts: 'TYPE_REWARDED_SURVEY,TYPE_REGISTRATION_WALL', - dismissedPromptGetCallCount: 2, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_NEWSLETTER_SIGNUP', - configurationId: 'newsletter_config_id', - onCancel: sandbox.match.any, - autoPromptType: undefined, - isClosable: true, - }); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_NEWSLETTER_SIGNUP' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'TYPE_REWARDED_SURVEY,TYPE_REGISTRATION_WALL,TYPE_NEWSLETTER_SIGNUP' - ); - }); - - it('should skip survey and show second Audience Action flow if survey was completed', async () => { - const storedSurveyCompleted = (CURRENT_TIME - 5).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 2, - storedSurveyCompleted, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: undefined, - isClosable: true, - }); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'TYPE_REGISTRATION_WALL' - ); - }); - - it('should skip survey and show second Audience Action flow if survey data transfer failed', async () => { - const storedSurveyFailed = (CURRENT_TIME - 5).toString(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 2, - storedSurveyFailed, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: undefined, - isClosable: true, - }); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'TYPE_REGISTRATION_WALL' - ); - }); - - it('should show nothing if the the last Audience Action was previously dismissed', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPrompts: - 'TYPE_REWARDED_SURVEY,TYPE_REGISTRATION_WALL,TYPE_NEWSLETTER_SIGNUP', - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_).to.equal(null); - }); - - [ - { - gaEligible: false, - gtagEligible: true, - }, - { - gaEligible: true, - gtagEligible: false, - }, - ].forEach(({gaEligible, gtagEligible}) => { - it(`should show survey if TYPE_REWARDED_SURVEY is next and is ga eligible ${gaEligible}, and is gTag eligible: ${gtagEligible}`, async () => { - setWinWithAnalytics(/* gtag */ gtagEligible, /* ga */ gaEligible); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 2, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: undefined, - isClosable: true, - }); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REWARDED_SURVEY' - ); - expect( - autoPromptManager.interventionDisplayed_.configurationId - ).to.equal('survey_config_id'); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'TYPE_REWARDED_SURVEY' - ); - }); - }); - - it('should skip action and continue the Contribution Flow if TYPE_REWARDED_SURVEY is next but publisher is not eligible for ga nor gTag', async () => { - setWinWithAnalytics(/* gtag */ false, /* ga */ false); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 2, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - miniPromptApiMock.expects('create').never(); - - await autoPromptManager.showAutoPrompt({ - // autoPromptType value not provided - alwaysShow: false, - }); - await tick(10); - - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: undefined, - isClosable: true, - }); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REGISTRATION_WALL' - ); - await verifyOnCancelStores( - storageMock, - actionFlowSpy, - 'TYPE_REGISTRATION_WALL' - ); - }); - }); - - describe('Prompt Frequency Capping Flow', () => { - let autoPromptConfig; - let getArticleExpectation; - let getClientConfigExpectation; - const globalFrequencyCapDurationSeconds = 100; - const contributionFrequencyCapDurationSeconds = 10800; - const surveyFrequencyCapDurationSeconds = 7200; - const newsletterFrequencyCapDurationSeconds = 3600; - const promptFrequencyCaps = [ - { - audienceActionType: CONTRIBUTION_INTERVENTION.type, - frequencyCapDuration: { - seconds: contributionFrequencyCapDurationSeconds, - }, - }, - { - audienceActionType: SURVEY_INTERVENTION.type, - frequencyCapDuration: {seconds: surveyFrequencyCapDurationSeconds}, - }, - { - audienceActionType: NEWSLETTER_INTERVENTION.type, - frequencyCapDuration: {seconds: newsletterFrequencyCapDurationSeconds}, - }, - ]; - const anyPromptFrequencyCapDurationSeconds = 600; - - beforeEach(() => { - autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - globalFrequencyCapDurationSeconds, - promptFrequencyCaps, - anyPromptFrequencyCapDurationSeconds, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - uiPredicates, - useUpdatedOfferFlows: true, - }); - getClientConfigExpectation = - clientConfigManagerMock.expects('getClientConfig'); - getClientConfigExpectation.resolves(clientConfig).once(); - const entitlements = new Entitlements(); - sandbox.stub(entitlements, 'enablesThis').returns(false); - entitlementsManagerMock - .expects('getEntitlements') - .resolves(entitlements) - .once(); - getArticleExpectation = entitlementsManagerMock.expects('getArticle'); - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - CONTRIBUTION_INTERVENTION, - SURVEY_INTERVENTION, - NEWSLETTER_INTERVENTION, - ], - engineId: '123', - }, - experimentConfig: { - experimentFlags: [ - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, - }) - .once(); - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = false; - }); - - it('should execute the legacy triggering flow if there is no frequency cap config', async () => { - autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - uiPredicates, - useUpdatedOfferFlows: true, - }); - getClientConfigExpectation.resolves(clientConfig).once(); - - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(10); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(contributionPromptFnSpy).to.have.been.calledOnce; - }); - - it('should set promptIsFromCtaButton_ to false when prompt is displayed', async () => { - autoPromptManager.promptIsFromCtaButton_ = true; - - await autoPromptManager.showAutoPrompt({}); - await tick(20); - - expect(autoPromptManager.promptIsFromCtaButton_).to.be.false; - }); - - it('should not show any prompt if there are no audience actions', async () => { - getArticleExpectation - .resolves({ - audienceActions: { - actions: [], - engineId: '123', - }, - experimentConfig: { - experimentFlags: [ - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, - }) - .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - }); - - it('should not show any prompt if there are no eligible audience actions', async () => { - setWinWithAnalytics(/* gtag */ false, /* ga */ false); - getArticleExpectation - .resolves({ - audienceActions: { - actions: [SURVEY_INTERVENTION], - engineId: '123', - }, - experimentConfig: { - experimentFlags: [ - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, - }) - .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - }); - - it('should show the first prompt if there are no stored impressions', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.have.been.calledOnce; - }); - - it('should show the first prompt if the frequency cap is not met', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const contributionTimestamps = ( - CURRENT_TIME - - 10 * contributionFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contributionTimestamps) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: contributionTimestamps, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.have.been.calledOnce; - }); - - it('should show the first contribution prompt if it is not dismissible', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - isClosable: false, - }); - await tick(20); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.have.been.calledOnce; - }); - - it('should show the second prompt if the frequency cap for contributions is met', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const contributionTimestamps = ( - CURRENT_TIME - - (contributionFrequencyCapDurationSeconds - 1) * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contributionTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: contributionTimestamps, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(25); - - expect(logEventSpy).to.be.calledOnceWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, - }); - }); - - it('should show the second prompt if the global frequency cap is undefined and prompt frequency cap for contributions is met', async () => { - autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - promptFrequencyCaps, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - uiPredicates, - useUpdatedOfferFlows: true, - }); - getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const contributionTimestamps = ( - CURRENT_TIME - - (contributionFrequencyCapDurationSeconds - 1) * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contributionTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); - - expect(logEventSpy).to.be.calledOnceWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, - }); - }); - - it('should show the second prompt if the frequency cap for contributions is undefined and the default anyPromptFrequencyCap is met', async () => { - autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - globalFrequencyCapDurationSeconds, - anyPromptFrequencyCapDurationSeconds, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - uiPredicates, - useUpdatedOfferFlows: true, - }); - getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const contributionTimestamps = ( - CURRENT_TIME - - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contributionTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: contributionTimestamps, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(25); - - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CONFIG_NOT_FOUND, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, - }); - }); - - it('should show the third prompt if the frequency cap for contributions is met and survey analytics is not configured', async () => { - setWinWithAnalytics(/* gtag */ false, /* ga */ false); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - const contributionTimestamps = ( - CURRENT_TIME - - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contributionTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.NEWSLETTER_SIGNUP, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: contributionTimestamps, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); - - expect(logEventSpy).to.be.calledOnceWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_NEWSLETTER_SIGNUP', - configurationId: 'newsletter_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, - }); - }); - - it('should show the third prompt if the frequency caps for contributions and surveys are met', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const promptTimestamps = ( - CURRENT_TIME - - (surveyFrequencyCapDurationSeconds - 1) * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.NEWSLETTER_SIGNUP, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: promptTimestamps, - survey: promptTimestamps, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(30); - - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_NEWSLETTER_SIGNUP', - configurationId: 'newsletter_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, - }); - }); - - it('should show the third prompt if the frequency caps for contributions and surveys are undefined and the default anyPromptFrequencyCap is met', async () => { - autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - globalFrequencyCapDurationSeconds, - anyPromptFrequencyCapDurationSeconds, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - uiPredicates, - useUpdatedOfferFlows: true, - }); - getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const promptTimestamps = ( - CURRENT_TIME - - (anyPromptFrequencyCapDurationSeconds - 1) * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.NEWSLETTER_SIGNUP, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: promptTimestamps, - survey: promptTimestamps, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(30); - - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_NEWSLETTER_SIGNUP', - configurationId: 'newsletter_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, - }); - }); - - it('should not show any prompt if the global frequency cap is met', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const timestampsWithinGlobalFrequencyCap = ( - CURRENT_TIME - - 0.5 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(timestampsWithinGlobalFrequencyCap) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: timestampsWithinGlobalFrequencyCap, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(50); - - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_GLOBAL_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - }); - - it('should not show any prompt if the global frequency cap is met via nanos', async () => { - autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - anyPromptFrequencyCapDurationNano: - anyPromptFrequencyCapDurationSeconds * SECOND_IN_NANO, - globalFrequencyCapDurationNano: - globalFrequencyCapDurationSeconds * SECOND_IN_NANO, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - uiPredicates, - useUpdatedOfferFlows: true, - }); - getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const timestampsWithinGlobalFrequencyCap = ( - CURRENT_TIME - - 0.5 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(timestampsWithinGlobalFrequencyCap) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: timestampsWithinGlobalFrequencyCap, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(50); - - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_GLOBAL_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - }); - - it('should not show any prompt if the frequency cap is met for all prompts (but global cap is not)', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const promptTimestamps = ( - CURRENT_TIME - - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.NEWSLETTER_SIGNUP, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(50); - - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - }); - - it('should not show any prompt if the frequency cap undefined for all prompts and the default anyPromptFrequencyCap is met (but global cap is not)', async () => { - autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - globalFrequencyCapDurationSeconds, - anyPromptFrequencyCapDurationSeconds, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - uiPredicates, - useUpdatedOfferFlows: true, - }); - getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const promptTimestamps = ( - CURRENT_TIME - - (anyPromptFrequencyCapDurationSeconds - 1) * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.NEWSLETTER_SIGNUP, - /* useLocalStorage */ true - ) - .resolves(promptTimestamps) - .once(); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); - - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, + eventType: AnalyticsEvent.ACTION_SURVEY_SUBMIT_CLICK, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, additionalParameters: null, - timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; }); - it('should show the second dismissible prompt if the frequency cap for contributions is met on locked content', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const contributionTimestamps = ( - CURRENT_TIME - - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); + [ + { + eventType: AnalyticsEvent.EVENT_PAYMENT_FAILED, + autoPromptType: AutoPromptType.CONTRIBUTION, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.EVENT_PAYMENT_FAILED, + autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.EVENT_PAYMENT_FAILED, + autoPromptType: AutoPromptType.SUBSCRIPTION, + action: 'TYPE_SUBSCRIPTION', + }, + { + eventType: AnalyticsEvent.EVENT_PAYMENT_FAILED, + autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, autoPromptType, action}) => { + it(`for generic eventType=${eventType}, should set completions via local storage for autoPromptType=${autoPromptType}`, async () => { + autoPromptManager.isClosable_ = true; + autoPromptManager.autoPromptType_ = autoPromptType; + expectFrequencyCappingTimestamps(storageMock, '', { + [action]: {completions: [CURRENT_TIME]}, + }); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + }); + }); + + it(`should set all event timestamps for a given prompt on storing completions`, async () => { + autoPromptManager.isClosable_ = true; storageMock .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contributionTimestamps) + .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) + .resolves('') .once(); storageMock - .expects('get') + .expects('set') .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, + StorageKeys.TIMESTAMPS, + JSON.stringify({ + 'TYPE_REWARDED_SURVEY': { + impressions: [], + dismissals: [], + completions: [CURRENT_TIME], + }, + }), /* useLocalStorage */ true ) .resolves(null) .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: contributionTimestamps, - }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(50); + await autoPromptManager.storeCompletion('TYPE_REWARDED_SURVEY'); + }); + }); - expect(logEventSpy).to.be.calledOnceWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, + describe('Miniprompt', () => { + it('should display the mini prompt, but not fetch entitlements and client config if alwaysShow is enabled', async () => { + entitlementsManagerMock.expects('getEntitlements').never(); + clientConfigManagerMock.expects('getAutoPromptConfig').never(); + miniPromptApiMock.expects('create').once(); + + await autoPromptManager.showAutoPrompt({ + autoPromptType: AutoPromptType.CONTRIBUTION, + alwaysShow: true, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(autoPromptManager.isClosable_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, + }); + + it('should display the large prompt, but not fetch entitlements and client config if alwaysShow is enabled', async () => { + entitlementsManagerMock.expects('getEntitlements').never(); + clientConfigManagerMock.expects('getAutoPromptConfig').never(); + miniPromptApiMock.expects('create').never(); + + await autoPromptManager.showAutoPrompt({ autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, + alwaysShow: true, }); }); - it('should not show any dismissible prompt if the global frequency cap is met on locked content', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const contributionTimestsamps = ( - CURRENT_TIME - - 0.5 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contributionTimestsamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: ( - CURRENT_TIME - - 0.5 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(), + it('should not display a prompt if the autoprompttype is unknown and alwaysShow is enabled', async () => { + entitlementsManagerMock.expects('getEntitlements').never(); + clientConfigManagerMock.expects('getAutoPromptConfig').never(); + miniPromptApiMock.expects('create').never(); + + await autoPromptManager.showAutoPrompt({ + autoPromptType: 'UNKNOWN', + alwaysShow: true, }); + }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(50); + it('should not display a prompt if autoprompttype is NONE', async () => { + entitlementsManagerMock.expects('getEntitlements').never(); + entitlementsManagerMock.expects('getArticle').never(); + clientConfigManagerMock.expects('getClientConfig').never(); + storageMock.expects('get').never(); + miniPromptApiMock.expects('create').never(); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_GLOBAL_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, + await autoPromptManager.showAutoPrompt({ + autoPromptType: AutoPromptType.NONE, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(autoPromptManager.isClosable_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; + await tick(10); + expect(startSpy).to.not.have.been.called; expect(actionFlowSpy).to.not.have.been.called; + expect(autoPromptManager.getLastAudienceActionFlow()).to.equal(null); + expect(contributionPromptFnSpy).to.not.be.called; + expect(subscriptionPromptFnSpy).to.not.be.called; }); - it('should show the contribution as a mini prompt', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - expectFrequencyCappingGlobalImpressions(storageMock); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(null) + it('should not display any prompt if canDisplayAutoPrompt is false', async () => { + const entitlements = new Entitlements(); + entitlementsManagerMock + .expects('getEntitlements') + .resolves(entitlements) .once(); - miniPromptApiMock.expects('create').once(); + entitlementsManagerMock + .expects('getArticle') + .resolves({ + audienceActions: { + actions: [CONTRIBUTION_INTERVENTION], + engineId: '123', + }, + }) + .once(); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ false); + const clientConfig = new ClientConfig({uiPredicates}); + clientConfigManagerMock + .expects('getClientConfig') + .returns(clientConfig) + .once(); + miniPromptApiMock.expects('create').never(); + + await autoPromptManager.showAutoPrompt({}); + await tick(10); + + expect(contributionPromptFnSpy).to.not.be.called; + expect(subscriptionPromptFnSpy).to.not.be.called; + }); + + it('should not display the mini contribution prompt if the article is null', async () => { + const entitlements = new Entitlements(); + entitlementsManagerMock + .expects('getEntitlements') + .resolves(entitlements) + .once(); + entitlementsManagerMock.expects('getArticle').resolves(null).once(); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({uiPredicates}); + clientConfigManagerMock + .expects('getClientConfig') + .resolves(clientConfig) + .once(); + miniPromptApiMock.expects('create').never(); await autoPromptManager.showAutoPrompt({ autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, }); - await tick(20); + await tick(10); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; + expect(contributionPromptFnSpy).to.not.be.called; }); - it('should show the first nondismissible subscription prompt for metered flow', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - getArticleExpectation + it('should not display the mini contribution prompt if the article returns no actions', async () => { + const entitlements = new Entitlements(); + entitlementsManagerMock + .expects('getEntitlements') + .resolves(entitlements) + .once(); + entitlementsManagerMock.expects('getArticle').resolves({}).once(); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({uiPredicates}); + clientConfigManagerMock + .expects('getClientConfig') + .resolves(clientConfig) + .once(); + miniPromptApiMock.expects('create').never(); + + await autoPromptManager.showAutoPrompt({ + autoPromptType: AutoPromptType.CONTRIBUTION, + }); + await tick(10); + + expect(contributionPromptFnSpy).to.not.be.called; + }); + + it('should display the subscription mini prompt if the user has no entitlements', async () => { + const entitlements = new Entitlements(); + entitlementsManagerMock + .expects('getEntitlements') + .resolves(entitlements) + .once(); + entitlementsManagerMock + .expects('getArticle') .resolves({ audienceActions: { - actions: [ - SURVEY_INTERVENTION, - REGWALL_INTERVENTION, - SUBSCRIPTION_INTERVENTION, - ], + actions: [SUBSCRIPTION_INTERVENTION], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], + }) + .once(); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({uiPredicates}); + clientConfigManagerMock + .expects('getClientConfig') + .resolves(clientConfig) + .once(); + miniPromptApiMock.expects('create').once(); + + await autoPromptManager.showAutoPrompt({ + autoPromptType: AutoPromptType.SUBSCRIPTION, + }); + await tick(10); + + expect(subscriptionPromptFnSpy).to.not.be.called; + }); + + it('should not display any prompt if the user has a valid entitlement', async () => { + const entitlements = new Entitlements(); + sandbox.stub(entitlements, 'enablesThis').returns(true); + entitlementsManagerMock + .expects('getEntitlements') + .resolves(entitlements) + .once(); + entitlementsManagerMock + .expects('getArticle') + .resolves({ + audienceActions: { + actions: [CONTRIBUTION_INTERVENTION], + engineId: '123', }, }) .once(); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({uiPredicates}); + clientConfigManagerMock + .expects('getClientConfig') + .resolves(clientConfig) + .once(); + miniPromptApiMock.expects('create').never(); + + await autoPromptManager.showAutoPrompt({ + autoPromptType: AutoPromptType.CONTRIBUTION, + }); + await tick(10); + + expect(contributionPromptFnSpy).to.not.be.called; + }); + + [ + { + autoPromptType: AutoPromptType.CONTRIBUTION, + }, + { + autoPromptType: AutoPromptType.SUBSCRIPTION, + }, + ].forEach(({autoPromptType}) => { + it(`should not display any monetization prompt if the article returns no actions for autoPromptType: ${autoPromptType}`, async () => { + const entitlements = new Entitlements(); + sandbox.stub(entitlements, 'enablesThis').returns(false); + entitlementsManagerMock + .expects('getEntitlements') + .resolves(entitlements) + .once(); + entitlementsManagerMock + .expects('getArticle') + .resolves({ + audienceActions: { + actions: [], // No action is eligible + engineId: '123', + }, + }) + .once(); + + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({ + useUpdatedOfferFlows: true, + uiPredicates, + }); + clientConfigManagerMock + .expects('getClientConfig') + .resolves(clientConfig) + .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); + miniPromptApiMock.expects('create').never(); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); + await autoPromptManager.showAutoPrompt({ + autoPromptType, + }); + await tick(10); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - isClosable: false, + expect(startSpy).to.not.have.been.called; + expect(actionFlowSpy).to.not.have.been.called; + expect(contributionPromptFnSpy).to.not.be.called; + expect(subscriptionPromptFnSpy).to.not.be.called; }); }); - it('should show the second dismissible prompt if the frequency cap is met for subscription openaccess content', async () => { - getArticleExpectation + it('should display the contribution mini prompt if the user has no entitlements and UI predicate is true', async () => { + const entitlements = new Entitlements(); + entitlementsManagerMock + .expects('getEntitlements') + .resolves(entitlements) + .once(); + entitlementsManagerMock + .expects('getArticle') .resolves({ audienceActions: { - actions: [ - SURVEY_INTERVENTION, - REGWALL_INTERVENTION, - SUBSCRIPTION_INTERVENTION, - ], + actions: [CONTRIBUTION_INTERVENTION], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, }) .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const surveyTimestamps = ( - CURRENT_TIME - - (surveyFrequencyCapDurationSeconds - 1) * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(surveyTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REGISTRATION_WALL, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - survey: surveyTimestamps, + + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({ + useUpdatedOfferFlows: true, + uiPredicates, }); + clientConfigManagerMock + .expects('getClientConfig') + .resolves(clientConfig) + .once(); + miniPromptApiMock.expects('create').once(); await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - isClosable: true, + autoPromptType: AutoPromptType.CONTRIBUTION, }); - await tick(50); + await tick(10); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CONFIG_NOT_FOUND, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, + expect(contributionPromptFnSpy).to.not.be.called; + }); + + [ + { + miniPromptEventType: + AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, + largePromptEventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, + dismissableEventType: AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED, + autoPromptType: 'TYPE_CONTRIBUTION', + }, + { + miniPromptEventType: + AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT, + largePromptEventType: AnalyticsEvent.IMPRESSION_OFFERS, + dismissableEventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, + autoPromptType: 'TYPE_SUBSCRIPTION', + }, + ].forEach((params) => { + const { + miniPromptEventType, + largePromptEventType, + dismissableEventType, + autoPromptType, + } = params; + it(`should not store an impression for ${autoPromptType} if a previous miniprompt impression has been stored`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps(storageMock, '', { + [autoPromptType]: {impressions: [CURRENT_TIME]}, + }); + expectFrequencyCappingTimestamps( + storageMock, + {[autoPromptType]: {impressions: [CURRENT_TIME]}}, + { + [autoPromptType]: { + impressions: [CURRENT_TIME], + dismissals: [CURRENT_TIME], + }, + } + ); + + await eventManagerCallback({ + eventType: miniPromptEventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + await eventManagerCallback({ + eventType: largePromptEventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + await eventManagerCallback({ + eventType: dismissableEventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(autoPromptManager.isClosable_).to.equal(true); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REGISTRATION_WALL', - configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - isClosable: true, + }); + + // Impression Events + [ + {eventType: AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT}, + {eventType: AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT}, + ].forEach(({eventType}) => { + it(`should not store miniprompt impression timestamps for event ${eventType} for nondismissible prompts`, async () => { + autoPromptManager.isClosable_ = false; + storageMock.expects('get').never(); + storageMock.expects('set').never(); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); }); - it('should not show any prompt if the global frequency cap is met for subscription openaccess content', async () => { - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - SURVEY_INTERVENTION, - REGWALL_INTERVENTION, - SUBSCRIPTION_INTERVENTION, - ], - engineId: '123', - }, - experimentConfig: { - experimentFlags: [ - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, - }) - .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const surveyTimestamps = ( - CURRENT_TIME - - 0.5 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(surveyTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REGISTRATION_WALL, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - expectFrequencyCappingGlobalImpressions(storageMock, { - survey: surveyTimestamps, + [ + { + eventType: AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, action}) => { + it(`for miniprompt eventType=${eventType}, should set impression timestamps for action=${action}`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps(storageMock, '', { + [action]: {impressions: [CURRENT_TIME]}, + }); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); + }); - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - isClosable: true, + // Dismissal Events + [ + {eventType: AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED}, + {eventType: AnalyticsEvent.ACTION_NEWSLETTER_OPT_IN_CLOSE}, + {eventType: AnalyticsEvent.ACTION_BYOP_NEWSLETTER_OPT_IN_CLOSE}, + {eventType: AnalyticsEvent.ACTION_REGWALL_OPT_IN_CLOSE}, + {eventType: AnalyticsEvent.ACTION_SURVEY_CLOSED}, + {eventType: AnalyticsEvent.ACTION_REWARDED_AD_CLOSE}, + {eventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED}, + ].forEach(({eventType}) => { + it(`should not store dismissal timestamps for miniprompt event ${eventType} for nondismissible prompts`, async () => { + autoPromptManager.isClosable_ = false; + storageMock.expects('get').never(); + storageMock.expects('set').never(); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); - await tick(50); + }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, + [ + { + eventType: AnalyticsEvent.ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLOSE, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.ACTION_SWG_SUBSCRIPTION_MINI_PROMPT_CLOSE, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, action}) => { + it(`for miniprompt eventType=${eventType}, should set dismissal timestamps for action=${action}`, async () => { + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps(storageMock, '', { + [action]: {dismissals: [CURRENT_TIME]}, + }); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_GLOBAL_FREQUENCY_CAP_MET, + }); + + it('should log events when a large prompt overrides the miniprompt', async () => { + win./*OK*/ innerWidth = 500; + setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); + const expectedEvent = { + eventType: AnalyticsEvent.EVENT_DISABLE_MINIPROMPT_DESKTOP, eventOriginator: EventOriginator.SWG_CLIENT, isFromUserAction: false, - additionalParameters: null, + additionalParameters: { + publicationid: pubId, + promptType: AutoPromptType.CONTRIBUTION, + }, timestamp: sandbox.match.number, + }; + + await autoPromptManager.showAutoPrompt({ + autoPromptType: AutoPromptType.CONTRIBUTION, + alwaysShow: true, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(autoPromptManager.isClosable_).to.equal(true); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - }); + await tick(10); - it('should execute the legacy frequency cap flow if the experiment is disabled', async () => { - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - CONTRIBUTION_INTERVENTION, - SURVEY_INTERVENTION, - NEWSLETTER_INTERVENTION, - ], - engineId: '123', - }, - }) - .once(); + expect(logEventSpy).to.be.calledOnceWith(expectedEvent); + }); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); + it('should replace the contribution miniprompt with a large prompt if DISABLE_DESKTOP_MINIPROMPT is enabled and viewport is wider than 480px', async () => { + win./*OK*/ innerWidth = 500; + setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); + miniPromptApiMock.expects('create').never(); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({ + autoPromptType: AutoPromptType.CONTRIBUTION, + alwaysShow: true, + }); await tick(10); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(false); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(autoPromptManager.interventionDisplayed_?.type).to.equal( - 'TYPE_CONTRIBUTION' - ); - expect(contributionPromptFnSpy).to.have.been.calledOnce; + expect(contributionPromptFnSpy).to.be.calledOnce; }); - it('should execute the legacy subscription flow if the experiment is disabled', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - SURVEY_INTERVENTION, - REGWALL_INTERVENTION, - SUBSCRIPTION_INTERVENTION, - ], - engineId: '123', - }, - }) - .once(); - - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); + it('should replace the subscription miniprompt with a large prompt if DISABLE_DESKTOP_MINIPROMPT is enabled and viewport is wider than 480px', async () => { + win./*OK*/ innerWidth = 500; + setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); + miniPromptApiMock.expects('create').never(); await autoPromptManager.showAutoPrompt({ - alwaysShow: false, + autoPromptType: AutoPromptType.SUBSCRIPTION, + alwaysShow: true, }); await tick(10); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(false); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - isClosable: false, - }); + expect(subscriptionPromptFnSpy).to.be.calledOnce; }); - it('should display a monetization prompt for an unknown autoprompt type if the next action is a monetization prompt', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, + it('should not replace the miniprompt with a large prompt when DISABLE_DESKTOP_MINIPROMPT is enabled but the viewport is narrower than 480px', async () => { + win./*OK*/ innerWidth = 450; + setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); + const expectedEvent = { + eventType: AnalyticsEvent.EVENT_DISABLE_MINIPROMPT_DESKTOP, + eventOriginator: EventOriginator.SWG_CLIENT, + isFromUserAction: false, + additionalParameters: { + publicationid: pubId, + promptType: AutoPromptType.CONTRIBUTION, }, - /* setAutopromptExpectations */ false - ); - expectFrequencyCappingGlobalImpressions(storageMock); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); + }; + miniPromptApiMock.expects('create').once(); await autoPromptManager.showAutoPrompt({ - autoPromptType: 'unknown', - alwaysShow: false, + autoPromptType: AutoPromptType.CONTRIBUTION, + alwaysShow: true, }); - await tick(25); + await tick(10); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect(contributionPromptFnSpy).to.be.calledOnce; - expect(startSpy).to.not.have.been.called; + logEventSpy.should.not.have.been.calledWith(expectedEvent); + expect(contributionPromptFnSpy).to.not.be.called; }); + }); - it('should display a dismissible prompt for an unknown autoprompt type if the next action is a nonmonetization prompt', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - const contributionTimestamps = ( - CURRENT_TIME - - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS - ).toString(); - expectFrequencyCappingGlobalImpressions(storageMock, { - contribution: contributionTimestamps, + describe('Call to Action (CTA) Button', () => { + [ + { + eventType: AnalyticsEvent.ACTION_SWG_BUTTON_SHOW_OFFERS_CLICK, + }, + { + eventType: AnalyticsEvent.ACTION_SWG_BUTTON_SHOW_CONTRIBUTIONS_CLICK, + }, + ].forEach(({eventType}) => { + it(`should set promptIsFromCtaButton on cta button action: ${eventType}`, async () => { + autoPromptManager.frequencyCappingLocalStorageEnabled_ = true; + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + expect(autoPromptManager.promptIsFromCtaButton_).to.be.true; }); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contributionTimestamps) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); + }); - await autoPromptManager.showAutoPrompt({ - autoPromptType: 'unknown', - alwaysShow: false, + [ + { + eventType: AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.IMPRESSION_OFFERS, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, action}) => { + it(`for autoprompt eventType=${eventType} and promptIsFromCta_ = true, should not set impressions for action=${action}`, async () => { + autoPromptManager.promptIsFromCtaButton_ = true; + autoPromptManager.isClosable_ = true; + storageMock.expects('get').never(); + storageMock.expects('set').never(); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); - await tick(25); + }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, + [ + { + eventType: AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, action}) => { + it(`for autoprompt eventType=${eventType} and promptIsFromCta_ = true, should set dismissals for action=${action}`, async () => { + autoPromptManager.promptIsFromCtaButton_ = true; + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps(storageMock, '', { + [action]: {dismissals: [CURRENT_TIME]}, + }); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); + }); + }); + + [ + { + eventType: AnalyticsEvent.EVENT_CONTRIBUTION_PAYMENT_COMPLETE, + action: 'TYPE_CONTRIBUTION', + }, + { + eventType: AnalyticsEvent.EVENT_SUBSCRIPTION_PAYMENT_COMPLETE, + action: 'TYPE_SUBSCRIPTION', + }, + ].forEach(({eventType, action}) => { + it(`for autoprompt eventType=${eventType} and promptIsFromCta_ = true, should set completion for action=${action}`, async () => { + autoPromptManager.promptIsFromCtaButton_ = true; + autoPromptManager.isClosable_ = true; + expectFrequencyCappingTimestamps(storageMock, '', { + [action]: {completions: [CURRENT_TIME]}, + }); + + await eventManagerCallback({ + eventType, + eventOriginator: EventOriginator.UNKNOWN_CLIENT, + isFromUserAction: null, + additionalParameters: null, + }); }); }); }); - describe('Prompt Frequency Capping Flow by Dismissals and Completions, Impressions between different prompts', () => { + describe('Prompt Frequency Capping Flow', () => { let autoPromptConfig; let getArticleExpectation; let getClientConfigExpectation; @@ -5401,13 +1343,6 @@ describes.realWin('AutoPromptManager', (env) => { beforeEach(() => { autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, globalFrequencyCapDurationSeconds, promptFrequencyCaps, anyPromptFrequencyCapDurationSeconds, @@ -5438,53 +1373,8 @@ describes.realWin('AutoPromptManager', (env) => { ], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'fcbd_exp', - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, }) .once(); - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = false; - }); - - it('should execute the legacy triggering flow if there is no frequency cap config', async () => { - autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); - const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); - const clientConfig = new ClientConfig({ - autoPromptConfig, - uiPredicates, - useUpdatedOfferFlows: true, - }); - getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(10); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(contributionPromptFnSpy).to.have.been.calledOnce; }); it('should not show any prompt if there are no audience actions', async () => { @@ -5494,29 +1384,14 @@ describes.realWin('AutoPromptManager', (env) => { actions: [], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'fcbd_exp', - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, }) .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); + storageMock.expects('get').never(); + storageMock.expects('set').never(); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(subscriptionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; @@ -5530,30 +1405,13 @@ describes.realWin('AutoPromptManager', (env) => { actions: [SURVEY_INTERVENTION], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'fcbd_exp', - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, }) .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - expectFrequencyCappingTimestamps(storageMock); + expectFrequencyCappingTimestamps(storageMock, {}); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(subscriptionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; @@ -5566,24 +1424,8 @@ describes.realWin('AutoPromptManager', (env) => { actions: [SURVEY_INTERVENTION], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'fcbd_exp', - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, }) .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_REWARDED_SURVEY': { impressions: [CURRENT_TIME], @@ -5591,44 +1433,68 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(subscriptionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; }); + it('should show the prompt after the specified delay', async () => { + const displayDelaySeconds = 99; + autoPromptConfig = new AutoPromptConfig({displayDelaySeconds}); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({ + autoPromptConfig, + uiPredicates, + useUpdatedOfferFlows: true, + }); + getClientConfigExpectation.resolves(clientConfig).once(); + expectFrequencyCappingTimestamps(storageMock); + winMock + .expects('setTimeout') + .withExactArgs(sandbox.match.any, displayDelaySeconds) + .once(); + + await autoPromptManager.showAutoPrompt({}); + await tick(20); + }); + + it('should show the first prompt and log an error if the FrequencyCapConfig is invalid', async () => { + autoPromptConfig = new AutoPromptConfig({}); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({ + autoPromptConfig, + uiPredicates, + useUpdatedOfferFlows: true, + }); + getClientConfigExpectation.resolves(clientConfig).once(); + expectFrequencyCappingTimestamps(storageMock); + + await autoPromptManager.showAutoPrompt({}); + await tick(20); + + expect(contributionPromptFnSpy).to.have.been.calledOnce; + expect(logEventSpy).to.be.calledOnceWith({ + eventType: AnalyticsEvent.EVENT_FREQUENCY_CAP_CONFIG_NOT_FOUND_ERROR, + eventOriginator: EventOriginator.SWG_CLIENT, + isFromUserAction: false, + additionalParameters: null, + timestamp: sandbox.match.number, + }); + }); + it('should show the first prompt if there are no stored impressions', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.have.been.calledOnce; }); it('should show the first prompt if the frequency cap is not met', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -5645,20 +1511,10 @@ describes.realWin('AutoPromptManager', (env) => { await autoPromptManager.showAutoPrompt({alwaysShow: false}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.have.been.calledOnce; }); it('should show the first prompt if the first prompt was abandoned', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -5670,42 +1526,19 @@ describes.realWin('AutoPromptManager', (env) => { await autoPromptManager.showAutoPrompt({alwaysShow: false}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.have.been.calledOnce; }); it('should show the first contribution prompt if it is not dismissible', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock); - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - isClosable: false, - }); + await autoPromptManager.showAutoPrompt({isClosable: false}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.have.been.calledOnce; }); it('should show the first prompt and log an error if the timestamps parsed from localstorage is invalid', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { notImpressions: [ @@ -5719,23 +1552,37 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.have.been.calledOnce; + expect(logEventSpy).to.be.calledOnceWith({ + eventType: AnalyticsEvent.EVENT_LOCAL_STORAGE_TIMESTAMPS_PARSING_ERROR, + eventOriginator: EventOriginator.SWG_CLIENT, + isFromUserAction: false, + additionalParameters: null, + timestamp: sandbox.match.number, + }); }); - it('should show the second prompt if the frequency cap for contributions is met', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, + it('should show the first prompt if frequency cap has passed', async () => { + const contributionTimestamps = + CURRENT_TIME - + (contributionFrequencyCapDurationSeconds + 1) * SECOND_IN_MS; + expectFrequencyCappingTimestamps(storageMock, { + 'TYPE_CONTRIBUTION': { + impressions: [contributionTimestamps], + dismissals: [contributionTimestamps], }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); + }); + + await autoPromptManager.showAutoPrompt({}); + await tick(20); + + expect(contributionPromptFnSpy).to.have.been.calledOnce; + }); + + it('should show the second prompt if the frequency cap for contributions is met', async () => { const contributionTimestamps = CURRENT_TIME - (contributionFrequencyCapDurationSeconds - 1) * SECOND_IN_MS; @@ -5746,8 +1593,8 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(25); + await autoPromptManager.showAutoPrompt({}); + await tick(20); expect(logEventSpy).to.be.calledOnceWith({ eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, @@ -5756,13 +1603,11 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_REWARDED_SURVEY', configurationId: 'survey_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, isClosable: true, }); @@ -5771,15 +1616,6 @@ describes.realWin('AutoPromptManager', (env) => { it('should show the second prompt if the frequency cap for contributions is met via completions', async () => { // Note, a contribution completion completes the funnel, but should be // treated the same as dismissals in terms of prompt frequency. - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); const contributionTimestamps = CURRENT_TIME - (contributionFrequencyCapDurationSeconds - 1) * SECOND_IN_MS; @@ -5790,7 +1626,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(25); expect(logEventSpy).to.be.calledOnceWith({ @@ -5800,13 +1636,11 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_REWARDED_SURVEY', configurationId: 'survey_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, isClosable: true, }); @@ -5814,13 +1648,6 @@ describes.realWin('AutoPromptManager', (env) => { it('should show the second prompt if the global frequency cap is undefined and prompt frequency cap for contributions is met', async () => { autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, promptFrequencyCaps, }); const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); @@ -5830,56 +1657,88 @@ describes.realWin('AutoPromptManager', (env) => { useUpdatedOfferFlows: true, }); getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - const contributionTimestamps = - CURRENT_TIME - - (contributionFrequencyCapDurationSeconds - 1) * SECOND_IN_MS; + const contributionTimestamps = + CURRENT_TIME - + (contributionFrequencyCapDurationSeconds - 1) * SECOND_IN_MS; + expectFrequencyCappingTimestamps(storageMock, { + 'TYPE_CONTRIBUTION': { + impressions: [contributionTimestamps], + dismissals: [contributionTimestamps], + }, + }); + + await autoPromptManager.showAutoPrompt({}); + await tick(20); + + expect(logEventSpy).to.be.calledOnceWith({ + eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, + eventOriginator: EventOriginator.SWG_CLIENT, + isFromUserAction: false, + additionalParameters: null, + timestamp: sandbox.match.number, + }); + expect(contributionPromptFnSpy).to.not.have.been.called; + expect(startSpy).to.have.been.calledOnce; + expect(actionFlowSpy).to.have.been.calledWith(deps, { + action: 'TYPE_REWARDED_SURVEY', + configurationId: 'survey_config_id', + autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, + isClosable: true, + }); + }); + + it('should show the second prompt if the frequency cap for contributions is undefined and the default anyPromptFrequencyCap is met', async () => { + autoPromptConfig = new AutoPromptConfig({ + globalFrequencyCapDurationSeconds, + anyPromptFrequencyCapDurationSeconds, + }); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({ + autoPromptConfig, + uiPredicates, + useUpdatedOfferFlows: true, + }); + getClientConfigExpectation.resolves(clientConfig).once(); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { - impressions: [contributionTimestamps], - dismissals: [contributionTimestamps], + impressions: [ + CURRENT_TIME - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, + ], + dismissals: [ + CURRENT_TIME - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, + ], }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(20); + await autoPromptManager.showAutoPrompt({}); + await tick(25); - expect(logEventSpy).to.be.calledOnceWith({ + expect(logEventSpy).to.be.calledWith({ + eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CONFIG_NOT_FOUND, + eventOriginator: EventOriginator.SWG_CLIENT, + isFromUserAction: false, + additionalParameters: null, + timestamp: sandbox.match.number, + }); + expect(logEventSpy).to.be.calledWith({ eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, eventOriginator: EventOriginator.SWG_CLIENT, isFromUserAction: false, additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_REWARDED_SURVEY', configurationId: 'survey_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, isClosable: true, }); }); - it('should show the second prompt if the frequency cap for contributions is undefined and the default anyPromptFrequencyCap is met', async () => { + it('should show the second prompt if the second prompt frequency has passed', async () => { autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, globalFrequencyCapDurationSeconds, anyPromptFrequencyCapDurationSeconds, }); @@ -5890,15 +1749,6 @@ describes.realWin('AutoPromptManager', (env) => { useUpdatedOfferFlows: true, }); getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -5908,9 +1758,19 @@ describes.realWin('AutoPromptManager', (env) => { CURRENT_TIME - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, ], }, + 'TYPE_REWARDED_SURVEY': { + impressions: [ + CURRENT_TIME - + (surveyFrequencyCapDurationSeconds + 1) * SECOND_IN_MS, + ], + dismissals: [ + CURRENT_TIME - + (surveyFrequencyCapDurationSeconds + 1) * SECOND_IN_MS, + ], + }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(25); expect(logEventSpy).to.be.calledWith({ @@ -5927,13 +1787,11 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_REWARDED_SURVEY', configurationId: 'survey_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, isClosable: true, }); @@ -5941,15 +1799,6 @@ describes.realWin('AutoPromptManager', (env) => { it('should show the third prompt if the frequency cap for contributions is met and survey analytics is not configured', async () => { setWinWithAnalytics(/* gtag */ false, /* ga */ false); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -5961,7 +1810,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(20); expect(logEventSpy).to.be.calledOnceWith({ @@ -5971,28 +1820,17 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_NEWSLETTER_SIGNUP', configurationId: 'newsletter_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, isClosable: true, }); }); it('should show the third prompt if the frequency caps for contributions and surveys are met', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -6016,7 +1854,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(30); expect(logEventSpy).to.be.calledWith({ @@ -6033,13 +1871,11 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_NEWSLETTER_SIGNUP', configurationId: 'newsletter_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, isClosable: true, }); @@ -6047,13 +1883,6 @@ describes.realWin('AutoPromptManager', (env) => { it('should show the third prompt if the frequency caps for contributions and surveys are undefined and the default anyPromptFrequencyCap is met', async () => { autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, globalFrequencyCapDurationSeconds, anyPromptFrequencyCapDurationSeconds, }); @@ -6064,15 +1893,68 @@ describes.realWin('AutoPromptManager', (env) => { useUpdatedOfferFlows: true, }); getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, + expectFrequencyCappingTimestamps(storageMock, { + 'TYPE_CONTRIBUTION': { + impressions: [ + CURRENT_TIME - + (anyPromptFrequencyCapDurationSeconds - 1) * SECOND_IN_MS, + ], + dismissals: [ + CURRENT_TIME - + (anyPromptFrequencyCapDurationSeconds - 1) * SECOND_IN_MS, + ], }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); + 'TYPE_REWARDED_SURVEY': { + impressions: [ + CURRENT_TIME - + (anyPromptFrequencyCapDurationSeconds - 1) * SECOND_IN_MS, + ], + dismissals: [ + CURRENT_TIME - + (anyPromptFrequencyCapDurationSeconds - 1) * SECOND_IN_MS, + ], + }, + }); + + await autoPromptManager.showAutoPrompt({}); + await tick(30); + + expect(logEventSpy).to.be.calledWith({ + eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, + eventOriginator: EventOriginator.SWG_CLIENT, + isFromUserAction: false, + additionalParameters: null, + timestamp: sandbox.match.number, + }); + expect(logEventSpy).to.be.calledWith({ + eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, + eventOriginator: EventOriginator.SWG_CLIENT, + isFromUserAction: false, + additionalParameters: null, + timestamp: sandbox.match.number, + }); + expect(contributionPromptFnSpy).to.not.have.been.called; + expect(startSpy).to.have.been.calledOnce; + expect(actionFlowSpy).to.have.been.calledWith(deps, { + action: 'TYPE_NEWSLETTER_SIGNUP', + configurationId: 'newsletter_config_id', + autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, + isClosable: true, + }); + }); + + it('should show the third prompt if the third prompt frequency has passed', async () => { + autoPromptConfig = new AutoPromptConfig({ + globalFrequencyCapDurationSeconds, + anyPromptFrequencyCapDurationSeconds, + }); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + const clientConfig = new ClientConfig({ + autoPromptConfig, + uiPredicates, + useUpdatedOfferFlows: true, + }); + getClientConfigExpectation.resolves(clientConfig).once(); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -6094,9 +1976,19 @@ describes.realWin('AutoPromptManager', (env) => { (anyPromptFrequencyCapDurationSeconds - 1) * SECOND_IN_MS, ], }, + 'TYPE_NEWSLETTER_SIGNUP': { + impressions: [ + CURRENT_TIME - + (anyPromptFrequencyCapDurationSeconds + 1) * SECOND_IN_MS, + ], + dismissals: [ + CURRENT_TIME - + (anyPromptFrequencyCapDurationSeconds + 1) * SECOND_IN_MS, + ], + }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(30); expect(logEventSpy).to.be.calledWith({ @@ -6113,28 +2005,17 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_NEWSLETTER_SIGNUP', configurationId: 'newsletter_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, isClosable: true, }); }); it('should not show any prompt if the global frequency cap is met', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -6151,7 +2032,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(50); expect(logEventSpy).to.be.calledWith({ @@ -6168,7 +2049,6 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; expect(actionFlowSpy).to.not.have.been.called; @@ -6176,13 +2056,6 @@ describes.realWin('AutoPromptManager', (env) => { it('should not show any prompt if the global frequency cap is met via nanos', async () => { autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, anyPromptFrequencyCapDurationNano: anyPromptFrequencyCapDurationSeconds * SECOND_IN_NANO, globalFrequencyCapDurationNano: @@ -6195,15 +2068,6 @@ describes.realWin('AutoPromptManager', (env) => { useUpdatedOfferFlows: true, }); getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -6217,7 +2081,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(50); expect(logEventSpy).to.be.calledWith({ @@ -6234,22 +2098,12 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; expect(actionFlowSpy).to.not.have.been.called; }); it('should not show any prompt if the frequency cap is met for all prompts (but global cap is not)', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -6283,7 +2137,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(50); expect(logEventSpy).to.be.calledWith({ @@ -6307,22 +2161,12 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; expect(actionFlowSpy).to.not.have.been.called; }); it('should not show any prompt if the frequency cap is met for all prompts via completions (but global cap is not)', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -6356,7 +2200,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(50); expect(logEventSpy).to.be.calledWith({ @@ -6380,7 +2224,6 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; expect(actionFlowSpy).to.not.have.been.called; @@ -6388,13 +2231,6 @@ describes.realWin('AutoPromptManager', (env) => { it('should not show any prompt if the frequency cap undefined for all prompts and the default anyPromptFrequencyCap is met (but global cap is not)', async () => { autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, globalFrequencyCapDurationSeconds, anyPromptFrequencyCapDurationSeconds, }); @@ -6405,15 +2241,6 @@ describes.realWin('AutoPromptManager', (env) => { useUpdatedOfferFlows: true, }); getClientConfigExpectation.resolves(clientConfig).once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -6447,7 +2274,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(20); expect(logEventSpy).to.be.calledWith({ @@ -6471,131 +2298,13 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - expect(actionFlowSpy).to.not.have.been.called; - }); - - it('should show the second dismissible prompt if the frequency cap for contributions is met on locked content', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - expectFrequencyCappingTimestamps(storageMock, { - 'TYPE_CONTRIBUTION': { - impressions: [ - CURRENT_TIME - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, - ], - dismissals: [ - CURRENT_TIME - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, - ], - }, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(50); - - expect(logEventSpy).to.be.calledOnceWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(autoPromptManager.isClosable_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - isClosable: true, - }); - }); - - it('should not show any dismissible prompt if the global frequency cap is met on locked content', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - expectFrequencyCappingTimestamps(storageMock, { - 'TYPE_CONTRIBUTION': { - impressions: [ - CURRENT_TIME - - 0.5 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, - ], - dismissals: [ - CURRENT_TIME - - 0.5 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, - ], - }, - }); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(50); - - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(logEventSpy).to.be.calledWith({ - eventType: AnalyticsEvent.EVENT_GLOBAL_FREQUENCY_CAP_MET, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(autoPromptManager.isClosable_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; expect(actionFlowSpy).to.not.have.been.called; }); - it('should show the contribution as a mini prompt', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - expectFrequencyCappingTimestamps(storageMock); - miniPromptApiMock.expects('create').once(); - - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION, - alwaysShow: false, - }); - await tick(20); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - }); - - it('should show the first nondismissible subscription prompt for metered flow', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); + it('should show the first nondismissible subscription prompt for metered flow despite past dismissals', async () => { + autoPromptManager.isClosable_ = false; getArticleExpectation .resolves({ audienceActions: { @@ -6606,43 +2315,33 @@ describes.realWin('AutoPromptManager', (env) => { ], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'fcbd_exp', - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, }) .once(); - - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, + expectFrequencyCappingTimestamps(storageMock, { + 'TYPE_CONTRIBUTION': { + impressions: [ + CURRENT_TIME - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, + ], + dismissals: [ + CURRENT_TIME - 2 * globalFrequencyCapDurationSeconds * SECOND_IN_MS, + ], }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); - expectFrequencyCappingTimestamps(storageMock); + }); - await autoPromptManager.showAutoPrompt({alwaysShow: false}); + await autoPromptManager.showAutoPrompt({}); await tick(20); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(subscriptionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_REWARDED_SURVEY', configurationId: 'survey_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, isClosable: false, }); }); - it('should show the second dismissible prompt if the frequency cap is met for subscription openaccess content', async () => { + it('should show the second dismissible prompt if the frequency cap is met for dismissible subscription', async () => { getArticleExpectation .resolves({ audienceActions: { @@ -6653,24 +2352,8 @@ describes.realWin('AutoPromptManager', (env) => { ], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'fcbd_exp', - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, }) .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_REWARDED_SURVEY': { impressions: [ @@ -6684,10 +2367,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - isClosable: true, - }); + await autoPromptManager.showAutoPrompt({isClosable: true}); await tick(50); expect(logEventSpy).to.be.calledWith({ @@ -6704,14 +2384,12 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(autoPromptManager.isClosable_).to.equal(true); expect(subscriptionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_REGISTRATION_WALL', configurationId: 'regwall_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, isClosable: true, }); @@ -6728,24 +2406,8 @@ describes.realWin('AutoPromptManager', (env) => { ], engineId: '123', }, - experimentConfig: { - experimentFlags: [ - 'fcbd_exp', - 'frequency_capping_local_storage_experiment', - 'prompt_frequency_capping_experiment', - ], - }, }) .once(); - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_REWARDED_SURVEY': { impressions: [ @@ -6759,10 +2421,7 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - isClosable: true, - }); + await autoPromptManager.showAutoPrompt({isClosable: true}); await tick(50); expect(logEventSpy).to.be.calledWith({ @@ -6772,128 +2431,24 @@ describes.realWin('AutoPromptManager', (env) => { additionalParameters: null, timestamp: sandbox.match.number, }); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(autoPromptManager.isClosable_).to.equal(true); expect(subscriptionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.not.have.been.called; expect(actionFlowSpy).to.not.have.been.called; }); - it('should execute the legacy frequency cap flow if the experiment is disabled', async () => { - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - CONTRIBUTION_INTERVENTION, - SURVEY_INTERVENTION, - NEWSLETTER_INTERVENTION, - ], - engineId: '123', - }, - }) - .once(); - - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ true, - /* setSurveyExpectations */ false - ); - - await autoPromptManager.showAutoPrompt({alwaysShow: false}); - await tick(10); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(false); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(true); - expect(autoPromptManager.interventionDisplayed_?.type).to.equal( - 'TYPE_CONTRIBUTION' - ); - expect(contributionPromptFnSpy).to.have.been.calledOnce; - }); - - it('should execute the legacy subscription flow if the experiment is disabled', async () => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - getArticleExpectation - .resolves({ - audienceActions: { - actions: [ - SURVEY_INTERVENTION, - REGWALL_INTERVENTION, - SUBSCRIPTION_INTERVENTION, - ], - engineId: '123', - }, - }) - .once(); - - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false - ); - - await autoPromptManager.showAutoPrompt({ - alwaysShow: false, - }); - await tick(10); - - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(false); - expect( - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ - ).to.equal(false); - expect(subscriptionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.have.been.calledOnce; - expect(actionFlowSpy).to.have.been.calledWith(deps, { - action: 'TYPE_REWARDED_SURVEY', - configurationId: 'survey_config_id', - onCancel: sandbox.match.any, - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - isClosable: false, - }); - }); - it('should display a monetization prompt for an unknown autoprompt type if the next action is a monetization prompt', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock); - await autoPromptManager.showAutoPrompt({ - autoPromptType: 'unknown', - alwaysShow: false, - }); + await autoPromptManager.showAutoPrompt({autoPromptType: 'unknown'}); await tick(25); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(subscriptionPromptFnSpy).to.not.have.been.called; expect(contributionPromptFnSpy).to.be.calledOnce; expect(startSpy).to.not.have.been.called; }); it('should display a dismissible prompt for an unknown autoprompt type if the next action is a nonmonetization prompt', async () => { - setupPreviousImpressionAndDismissals( - storageMock, - { - dismissedPromptGetCallCount: 1, - getUserToken: true, - }, - /* setAutopromptExpectations */ false, - /* setSurveyExpectations */ false - ); expectFrequencyCappingTimestamps(storageMock, { 'TYPE_CONTRIBUTION': { impressions: [ @@ -6907,19 +2462,14 @@ describes.realWin('AutoPromptManager', (env) => { }, }); - await autoPromptManager.showAutoPrompt({ - autoPromptType: 'unknown', - alwaysShow: false, - }); + await autoPromptManager.showAutoPrompt({autoPromptType: 'unknown'}); await tick(25); - expect(autoPromptManager.promptFrequencyCappingEnabled_).to.equal(true); expect(contributionPromptFnSpy).to.not.have.been.called; expect(startSpy).to.have.been.calledOnce; expect(actionFlowSpy).to.have.been.calledWith(deps, { action: 'TYPE_REWARDED_SURVEY', configurationId: 'survey_config_id', - onCancel: sandbox.match.any, autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, isClosable: true, }); @@ -7065,42 +2615,6 @@ describes.realWin('AutoPromptManager', (env) => { expect(isFrequencyCapped).to.equal(false); }); - it('getActionImpressions_ should return empty timestamps and log error event if an action type does not map to the storage key', async () => { - const impressions = await autoPromptManager.getActionImpressions_( - 'undefined' - ); - expect(logEventSpy).to.be.calledOnceWith({ - eventType: - AnalyticsEvent.EVENT_ACTION_IMPRESSIONS_STORAGE_KEY_NOT_FOUND_ERROR, - eventOriginator: EventOriginator.SWG_CLIENT, - isFromUserAction: false, - additionalParameters: null, - timestamp: sandbox.match.number, - }); - expect(impressions.length).to.equal(0); - }); - - it('isSurveyEligible_ returns false when survey is not a potential audience action', async () => { - const isSurveyEligible = await autoPromptManager.isSurveyEligible_([ - CONTRIBUTION_INTERVENTION, - ]); - expect(isSurveyEligible).to.equal(false); - }); - - it('getMonetizationPromptFun_ returns function that does not open a mini or large prompt with an undefined input', async () => { - miniPromptApiMock.expects('create').never(); - const fn = autoPromptManager.getMonetizationPromptFn_( - AutoPromptType.CONTRIBUTION, - undefined - ); - - await fn(); - await tick(10); - - expect(contributionPromptFnSpy).to.not.have.been.called; - expect(startSpy).to.not.have.been.called; - }); - it('getPotentialAction_ returns the first action and logs error event for contribution flow with no frequencyCapConfig', async () => { autoPromptManager.isClosable_ = true; const action = await autoPromptManager.getPotentialAction_({ @@ -7121,137 +2635,18 @@ describes.realWin('AutoPromptManager', (env) => { }); }); - function expectFrequencyCappingTimestamps( - storageMock, - get = {}, - set = undefined - ) { - if (get) { - get = JSON.stringify( - Object.entries(get).reduce((acc, [key, values]) => { - return { - ...acc, - [key]: { - impressions: [], - dismissals: [], - completions: [], - ...values, - }, - }; - }, {}) - ); - } - storageMock - .expects('get') - .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) - .resolves(get); - if (set != undefined) { - set = JSON.stringify( - Object.entries(set).reduce((acc, [key, values]) => { - return { - ...acc, - [key]: { - impressions: [], - dismissals: [], - completions: [], - ...values, - }, - }; - }, {}) - ); - storageMock - .expects('set') - .withExactArgs(StorageKeys.TIMESTAMPS, set, /* useLocalStorage */ true) - .resolves(null) - .once(); - } - } - - function expectFrequencyCappingGlobalImpressions( - storageMock, - impressions = {} - ) { - const {contribution, newsletter, regwall, survey, ad, subscription} = { - contribution: null, - newsletter: null, - regwall: null, - survey: null, - ad: null, - subscription: null, - ...impressions, - }; - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.CONTRIBUTION, - /* useLocalStorage */ true - ) - .resolves(contribution) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.NEWSLETTER_SIGNUP, - /* useLocalStorage */ true - ) - .resolves(newsletter) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REGISTRATION_WALL, - /* useLocalStorage */ true - ) - .resolves(regwall) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_SURVEY, - /* useLocalStorage */ true - ) - .resolves(survey) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.REWARDED_AD, - /* useLocalStorage */ true - ) - .resolves(ad) - .once(); - storageMock - .expects('get') - .withExactArgs( - ImpressionStorageKeys.SUBSCRIPTION, - /* useLocalStorage */ true - ) - .resolves(subscription) - .once(); - } - describe('AudienceActionLocalFlow', () => { let getArticleExpectation; let actionLocalFlowStub; let startLocalSpy; beforeEach(() => { - sandbox.stub(pageConfig, 'isLocked').returns(true); - autoPromptManager.monetizationPromptWasDisplayedAsSoftPaywall_ = false; const entitlements = new Entitlements(); entitlementsManagerMock .expects('getEntitlements') .resolves(entitlements) .once(); - const autoPromptConfig = new AutoPromptConfig({ - displayDelaySeconds: 0, - numImpressionsBetweenPrompts: 2, - dismissalBackOffSeconds: 5, - maxDismissalsPerWeek: 2, - maxDismissalsResultingHideSeconds: 10, - maxImpressions: 2, - maxImpressionsResultingHideSeconds: 10, - }); + const autoPromptConfig = new AutoPromptConfig({}); const uiPredicates = new UiPredicates( /* canDisplayAutoPrompt */ true, /* canDisplayButton */ true @@ -7292,9 +2687,7 @@ describes.realWin('AutoPromptManager', (env) => { getVersion: () => 'gpt_version_foo', }; - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - }); + await autoPromptManager.showAutoPrompt({}); await tick(7); @@ -7302,19 +2695,15 @@ describes.realWin('AutoPromptManager', (env) => { action: 'TYPE_REWARDED_AD', configurationId: 'rewarded_ad_config_id', autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, - onCancel: sandbox.match.any, isClosable: false, monetizationFunction: sandbox.match.any, }); expect(startLocalSpy).to.have.been.calledOnce; expect(startSpy).to.not.have.been.called; expect(autoPromptManager.getLastAudienceActionFlow()).to.not.equal(null); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_REWARDED_AD' - ); }); - it('is rendered for TYPE_NEWSLETTER_SIGNUP', async () => { + it('is rendered for BYOP TYPE_NEWSLETTER_SIGNUP', async () => { getArticleExpectation .resolves({ audienceActions: { @@ -7327,10 +2716,7 @@ describes.realWin('AutoPromptManager', (env) => { }) .once(); - await autoPromptManager.showAutoPrompt({ - autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - alwaysShow: false, - }); + await autoPromptManager.showAutoPrompt({}); await tick(7); @@ -7338,94 +2724,69 @@ describes.realWin('AutoPromptManager', (env) => { action: 'TYPE_NEWSLETTER_SIGNUP', configurationId: 'newsletter_config_id', autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, - onCancel: sandbox.match.any, isClosable: true, }); expect(startLocalSpy).to.have.been.calledOnce; expect(startSpy).to.not.have.been.called; expect(autoPromptManager.getLastAudienceActionFlow()).to.not.equal(null); - expect(autoPromptManager.interventionDisplayed_.type).to.equal( - 'TYPE_NEWSLETTER_SIGNUP' - ); }); }); - async function verifyOnCancelStores(storageMock, actionFlowSpy, setValue) { - storageMock - .expects('set') - .withExactArgs( - StorageKeys.DISMISSED_PROMPTS, - setValue, - /* useLocalStorage */ true - ) - .resolves(null) - .once(); - const {onCancel} = actionFlowSpy.firstCall.args[1]; - onCancel(); - await tick(2); + function setWinWithAnalytics(gtag, ga) { + const winWithAnalytics = Object.assign({}, win); + if (!gtag) { + delete winWithAnalytics.gtag; + } + if (!ga) { + delete winWithAnalytics.ga; + } + autoPromptManager.deps_.win.restore(); + sandbox.stub(autoPromptManager.deps_, 'win').returns(winWithAnalytics); } - function setupPreviousImpressionAndDismissals( + function expectFrequencyCappingTimestamps( storageMock, - setupArgs, - setAutopromptExpectations = true, - setSurveyExpectations = true + get = {}, + set = undefined ) { - const { - storedImpressions, - storedDismissals, - dismissedPrompts, - dismissedPromptGetCallCount, - storedSurveyCompleted, - storedSurveyFailed, - getUserToken, - } = { - storedImpressions: null, - storedDismissals: null, - dismissedPrompts: null, - storedSurveyCompleted: null, - storedSurveyFailed: null, - setsNewShouldShowAutoPromptTimestamp: false, - ...setupArgs, - }; - if (setAutopromptExpectations) { - storageMock - .expects('get') - .withExactArgs(StorageKeys.IMPRESSIONS, /* useLocalStorage */ true) - .resolves(storedImpressions) - .once(); - storageMock - .expects('get') - .withExactArgs(StorageKeys.DISMISSALS, /* useLocalStorage */ true) - .resolves(storedDismissals) - .once(); + if (get) { + get = JSON.stringify( + Object.entries(get).reduce((acc, [key, values]) => { + return { + ...acc, + [key]: { + impressions: [], + dismissals: [], + completions: [], + ...values, + }, + }; + }, {}) + ); } storageMock .expects('get') - .withExactArgs(StorageKeys.DISMISSED_PROMPTS, /* useLocalStorage */ true) - .resolves(dismissedPrompts) - .exactly(dismissedPromptGetCallCount); - if (setSurveyExpectations) { - storageMock - .expects('get') - .withExactArgs(StorageKeys.SURVEY_COMPLETED, /* useLocalStorage */ true) - .resolves(storedSurveyCompleted) - .once(); + .withExactArgs(StorageKeys.TIMESTAMPS, /* useLocalStorage */ true) + .resolves(get); + if (set != undefined) { + set = JSON.stringify( + Object.entries(set).reduce((acc, [key, values]) => { + return { + ...acc, + [key]: { + impressions: [], + dismissals: [], + completions: [], + ...values, + }, + }; + }, {}) + ); storageMock - .expects('get') - .withExactArgs( - StorageKeys.SURVEY_DATA_TRANSFER_FAILED, - /* useLocalStorage */ true - ) - .resolves(storedSurveyFailed) + .expects('set') + .withExactArgs(StorageKeys.TIMESTAMPS, set, /* useLocalStorage */ true) + .resolves(null) .once(); } - if (getUserToken) { - storageMock - .expects('get') - .withExactArgs(Constants.USER_TOKEN, /* useLocalStorage */ true) - .resolves('token') - .atMost(1); - } } }); diff --git a/src/runtime/auto-prompt-manager.ts b/src/runtime/auto-prompt-manager.ts index 14285811f2..6f9ff0f311 100644 --- a/src/runtime/auto-prompt-manager.ts +++ b/src/runtime/auto-prompt-manager.ts @@ -20,17 +20,11 @@ import { EntitlementsManager, Intervention, } from './entitlements-manager'; -import {ArticleExperimentFlags, ExperimentFlags} from './experiment-flags'; import { AudienceActionFlow, AudienceActionIframeFlow, } from './audience-action-flow'; import {AudienceActionLocalFlow} from './audience-action-local-flow'; -import { - AutoPromptConfig, - Duration, - FrequencyCapConfig, -} from '../model/auto-prompt-config'; import {AutoPromptType} from '../api/basic-subscriptions'; import {ClientConfig} from '../model/client-config'; import {ClientConfigManager} from './client-config-manager'; @@ -39,13 +33,15 @@ import {ClientEventManager} from './client-event-manager'; import {ConfiguredRuntime} from './runtime'; import {Deps} from './deps'; import {Doc} from '../model/doc'; +import {Duration, FrequencyCapConfig} from '../model/auto-prompt-config'; import {Entitlements} from '../api/entitlements'; +import {ExperimentFlags} from './experiment-flags'; import {GoogleAnalyticsEventListener} from './google-analytics-event-listener'; -import {ImpressionStorageKeys, StorageKeys} from '../utils/constants'; import {MiniPromptApi} from './mini-prompt-api'; import {OffersRequest} from '../api/subscriptions'; import {PageConfig} from '../model/page-config'; import {Storage, pruneTimestamps} from './storage'; +import {StorageKeys} from '../utils/constants'; import {assert} from '../utils/log'; import {isExperimentOn} from './experiments'; @@ -66,16 +62,6 @@ const monetizationImpressionEvents = [ AnalyticsEvent.IMPRESSION_OFFERS, AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, ]; -const dismissEvents = [ - AnalyticsEvent.ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLOSE, - AnalyticsEvent.ACTION_SWG_SUBSCRIPTION_MINI_PROMPT_CLOSE, - AnalyticsEvent.ACTION_CONTRIBUTION_OFFERS_CLOSED, - AnalyticsEvent.ACTION_SUBSCRIPTION_OFFERS_CLOSED, -]; - -const COMPLETED_ACTION_TO_STORAGE_KEY_MAP = new Map([ - [AnalyticsEvent.ACTION_SURVEY_DATA_TRANSFER, StorageKeys.SURVEY_COMPLETED], -]); const DISMISSAL_EVENTS_TO_ACTION_MAP = new Map([ [AnalyticsEvent.ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLOSE, TYPE_CONTRIBUTION], @@ -116,45 +102,6 @@ const IMPRESSION_EVENTS_TO_ACTION_MAP = new Map([ const GENERIC_COMPLETION_EVENTS = [AnalyticsEvent.EVENT_PAYMENT_FAILED]; -const INTERVENTION_TO_STORAGE_KEY_MAP = new Map([ - [ - AnalyticsEvent.IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT, - ImpressionStorageKeys.CONTRIBUTION, - ], - [ - AnalyticsEvent.IMPRESSION_CONTRIBUTION_OFFERS, - ImpressionStorageKeys.CONTRIBUTION, - ], - [ - AnalyticsEvent.IMPRESSION_NEWSLETTER_OPT_IN, - ImpressionStorageKeys.NEWSLETTER_SIGNUP, - ], - [ - AnalyticsEvent.IMPRESSION_BYOP_NEWSLETTER_OPT_IN, - ImpressionStorageKeys.NEWSLETTER_SIGNUP, - ], - [ - AnalyticsEvent.IMPRESSION_REGWALL_OPT_IN, - ImpressionStorageKeys.REGISTRATION_WALL, - ], - [AnalyticsEvent.IMPRESSION_SURVEY, ImpressionStorageKeys.REWARDED_SURVEY], - [AnalyticsEvent.IMPRESSION_REWARDED_AD, ImpressionStorageKeys.REWARDED_AD], - [ - AnalyticsEvent.IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT, - ImpressionStorageKeys.SUBSCRIPTION, - ], - [AnalyticsEvent.IMPRESSION_OFFERS, ImpressionStorageKeys.SUBSCRIPTION], -]); - -const ACTION_TO_IMPRESSION_STORAGE_KEY_MAP = new Map([ - [TYPE_CONTRIBUTION, ImpressionStorageKeys.CONTRIBUTION], - [TYPE_NEWSLETTER_SIGNUP, ImpressionStorageKeys.NEWSLETTER_SIGNUP], - [TYPE_REGISTRATION_WALL, ImpressionStorageKeys.REGISTRATION_WALL], - [TYPE_REWARDED_SURVEY, ImpressionStorageKeys.REWARDED_SURVEY], - [TYPE_REWARDED_AD, ImpressionStorageKeys.REWARDED_AD], - [TYPE_SUBSCRIPTION, ImpressionStorageKeys.SUBSCRIPTION], -]); - const ACTON_CTA_BUTTON_CLICK = [ AnalyticsEvent.ACTION_SWG_BUTTON_SHOW_OFFERS_CLICK, AnalyticsEvent.ACTION_SWG_BUTTON_SHOW_CONTRIBUTIONS_CLICK, @@ -181,15 +128,9 @@ interface ActionTimestamps { * displayed to the user. */ export class AutoPromptManager { - private monetizationPromptWasDisplayedAsSoftPaywall_ = false; - private hasStoredImpression_ = false; private hasStoredMiniPromptImpression_ = false; private promptIsFromCtaButton_ = false; private lastAudienceActionFlow_: AudienceActionFlow | null = null; - private interventionDisplayed_: Intervention | null = null; - private frequencyCappingByDismissalsEnabled_: boolean = false; - private frequencyCappingLocalStorageEnabled_: boolean = false; - private promptFrequencyCappingEnabled_: boolean = false; private isClosable_: boolean | undefined; private autoPromptType_: AutoPromptType | undefined; @@ -246,39 +187,29 @@ export class AutoPromptManager { // Manual override of display rules, mainly for demo purposes. Requires // contribution or subscription to be set as autoPromptType in snippet. if (params.alwaysShow) { - this.showPrompt_( + const promptFn = this.getMonetizationPromptFn_( this.getPromptTypeToDisplay_(params.autoPromptType), this.getLargeMonetizationPromptFn_( params.autoPromptType, params.isClosable ?? !this.isSubscription_(params.autoPromptType) ) ); + promptFn(); return; } // Fetch entitlements and the client config from the server, so that we have // the information we need to determine whether and which prompt should be // displayed. - const [clientConfig, entitlements, article, dismissedPrompts] = - await Promise.all([ - this.clientConfigManager_.getClientConfig(), - this.entitlementsManager_.getEntitlements(), - this.entitlementsManager_.getArticle(), - this.storage_.get( - StorageKeys.DISMISSED_PROMPTS, - /* useLocalStorage */ true - ), - ]); + const [clientConfig, entitlements, article] = await Promise.all([ + this.clientConfigManager_.getClientConfig(), + this.entitlementsManager_.getEntitlements(), + this.entitlementsManager_.getArticle(), + ]); this.setArticleExperimentFlags_(article); - this.showAutoPrompt_( - clientConfig, - entitlements, - article, - dismissedPrompts, - params - ); + this.showAutoPrompt_(clientConfig, entitlements, article, params); } /** @@ -288,20 +219,7 @@ export class AutoPromptManager { if (!article) { return; } - this.frequencyCappingByDismissalsEnabled_ = - this.isArticleExperimentEnabled_( - article, - ArticleExperimentFlags.FREQUENCY_CAPPING_BY_DISMISSALS - ); - this.frequencyCappingLocalStorageEnabled_ = - this.isArticleExperimentEnabled_( - article, - ArticleExperimentFlags.FREQUENCY_CAPPING_LOCAL_STORAGE - ); - this.promptFrequencyCappingEnabled_ = this.isArticleExperimentEnabled_( - article, - ArticleExperimentFlags.PROMPT_FREQUENCY_CAPPING_EXPERIMENT - ); + // Set experiment flags here. } /** @@ -312,7 +230,6 @@ export class AutoPromptManager { clientConfig: ClientConfig, entitlements: Entitlements, article: Article | null, - dismissedPrompts: string | undefined | null, params: ShowAutoPromptParams ): Promise { if (!article) { @@ -344,75 +261,22 @@ export class AutoPromptManager { // TODO(b/303489420): cleanup passing of autoPromptManager params. this.isClosable_ = isClosable; - // ** New Triggering Flow - Prompt Frequency Cap Experiment ** - // Guarded by experiment flag and presence of FrequencyCapConfig. Frequency - // cap flow utilizes config and impressions to determine next action. - // Metered flow strictly follows prompt order, with subscription last. - // Display delay is applied to all dismissible prompts. + // Frequency cap flow utilizes config and timestamps to determine next + // action. Metered flow strictly follows prompt order. Display delay is + // applied to all dismissible prompts. const frequencyCapConfig = clientConfig.autoPromptConfig?.frequencyCapConfig; - if ( - this.promptFrequencyCappingEnabled_ && - this.isValidFrequencyCap_(frequencyCapConfig) - ) { - const potentialAction = await this.getPotentialAction_({ - article, - frequencyCapConfig, - }); - - const promptFn = this.isMonetizationAction_(potentialAction?.type) - ? this.getMonetizationPromptFn_( - autoPromptType, - this.getLargeMonetizationPromptFn_(autoPromptType, isClosable) - ) - : potentialAction - ? this.audienceActionPrompt_({ - actionType: potentialAction.type, - configurationId: potentialAction.configurationId, - autoPromptType, - isClosable, - preference: potentialAction.preference, - }) - : undefined; - - if (!promptFn) { - return; - } - - this.promptIsFromCtaButton_ = false; - // Add display delay to dismissible prompts. - const displayDelayMs = isClosable - ? (clientConfig?.autoPromptConfig?.clientDisplayTrigger - ?.displayDelaySeconds || 0) * SECOND_IN_MILLIS - : 0; - this.deps_.win().setTimeout(promptFn, displayDelayMs); - return; - } - // Legacy Triggering Flow, to be deprecated after Prompt Frequency Cap - // flow is fully launched. - const canDisplayMonetizationPrompt = this.canDisplayMonetizationPrompt( - article?.audienceActions?.actions - ); - - const shouldShowMonetizationPromptAsSoftPaywall = - canDisplayMonetizationPrompt && - (await this.shouldShowMonetizationPromptAsSoftPaywall( - autoPromptType, - clientConfig.autoPromptConfig - )); - - const potentialAction = await this.getAction_({ + const potentialAction = await this.getPotentialAction_({ article, - autoPromptType, - dismissedPrompts, - canDisplayMonetizationPrompt, - shouldShowMonetizationPromptAsSoftPaywall, + frequencyCapConfig, }); - const promptFn = this.isMonetizationAction_(potentialAction?.type) - ? this.getLargeMonetizationPromptFn_(autoPromptType, isClosable) + ? this.getMonetizationPromptFn_( + autoPromptType, + this.getLargeMonetizationPromptFn_(autoPromptType, isClosable) + ) : potentialAction - ? this.audienceActionPrompt_({ + ? this.getAudienceActionPromptFn_({ actionType: potentialAction.type, configurationId: potentialAction.configurationId, autoPromptType, @@ -421,37 +285,18 @@ export class AutoPromptManager { }) : undefined; - const shouldShowBlockingPrompt = - this.shouldShowBlockingPrompt_( - /* hasPotentialAudienceAction */ potentialAction - ) && !!promptFn; - if ( - !shouldShowMonetizationPromptAsSoftPaywall && - !shouldShowBlockingPrompt - ) { + if (!promptFn) { return; } - const displayDelayMs = - (clientConfig?.autoPromptConfig?.clientDisplayTrigger - ?.displayDelaySeconds || 0) * SECOND_IN_MILLIS; - - if ( - shouldShowMonetizationPromptAsSoftPaywall && - this.isMonetizationAction_(potentialAction?.type) - ) { - this.deps_.win().setTimeout(() => { - this.monetizationPromptWasDisplayedAsSoftPaywall_ = true; - this.showPrompt_(autoPromptType, promptFn); - }, displayDelayMs); - } else if (promptFn) { - const isBlockingPromptWithDelay = this.isActionPromptWithDelay_( - potentialAction?.type - ); - this.deps_ - .win() - .setTimeout(promptFn, isBlockingPromptWithDelay ? displayDelayMs : 0); - } + this.promptIsFromCtaButton_ = false; + // Add display delay to dismissible prompts. + const displayDelayMs = isClosable + ? (clientConfig?.autoPromptConfig?.clientDisplayTrigger + ?.displayDelaySeconds || 0) * SECOND_IN_MILLIS + : 0; + this.deps_.win().setTimeout(promptFn, displayDelayMs); + return; } private isSubscription_(autoPromptType: AutoPromptType | undefined): boolean { @@ -472,176 +317,6 @@ export class AutoPromptManager { return actionType === TYPE_SUBSCRIPTION || actionType === TYPE_CONTRIBUTION; } - /** - * Returns a function that will call the mini prompt api with an eligible - * autoprompt type. - */ - private getMonetizationPromptFn_( - autoPromptType: AutoPromptType, - largeMonetizationPromptFn: (() => void) | undefined - ): () => void { - return () => { - if (!largeMonetizationPromptFn) { - return; - } - - if ( - autoPromptType === AutoPromptType.SUBSCRIPTION || - autoPromptType === AutoPromptType.CONTRIBUTION - ) { - this.miniPromptAPI_.create({ - autoPromptType, - clickCallback: largeMonetizationPromptFn, - }); - } else if ( - autoPromptType === AutoPromptType.SUBSCRIPTION_LARGE || - autoPromptType === AutoPromptType.CONTRIBUTION_LARGE - ) { - largeMonetizationPromptFn(); - } - }; - } - - /** - * Returns a function to show the appropriate monetization prompt, - * or undefined if the type of prompt cannot be determined. - */ - private getLargeMonetizationPromptFn_( - autoPromptType: AutoPromptType | undefined, - isClosable: boolean, - shouldAnimateFade: boolean = true - ): (() => void) | undefined { - const options: OffersRequest = {isClosable, shouldAnimateFade}; - if (this.isSubscription_(autoPromptType)) { - return () => { - this.configuredRuntime_.showOffers(options); - }; - } else if (this.isContribution_(autoPromptType)) { - return () => { - this.configuredRuntime_.showContributionOptions(options); - }; - } - return undefined; - } - - /** - * Determines whether moentization prompt can be shown based on audience actions - * that passed eligibility check. - */ - private canDisplayMonetizationPrompt(actions: Intervention[] = []): boolean { - return ( - actions.filter( - (action) => - action.type === TYPE_CONTRIBUTION || action.type === TYPE_SUBSCRIPTION - ).length > 0 - ); - } - - /** - * Determines whether a monetization prompt should be shown as a soft - * paywall, meaning with the explicit intent to soft-restrict access to the - * page. Does not prevent a monetization prompt from displaying as an - * eligible audience action (subscriptions as the last potential action). - */ - async shouldShowMonetizationPromptAsSoftPaywall( - autoPromptType?: AutoPromptType, - autoPromptConfig?: AutoPromptConfig - ): Promise { - // If the auto prompt type is not supported, don't show the prompt. - // AutoPromptType can be set undefined for premonetization publications; - // in this case, do not show a soft paywall. - if ( - autoPromptType === undefined || - autoPromptType === AutoPromptType.NONE - ) { - return false; - } - - // For paygated content, a soft paywall should not restrict access. - if (this.pageConfig_.isLocked()) { - return false; - } - - // Do not frequency cap subscription prompts as soft paywall. - if (this.isSubscription_(autoPromptType)) { - return true; - } - - // For other contributions, if no auto prompt config was returned, do not - // show a soft paywall. - if (autoPromptConfig === undefined) { - return false; - } - - // Fetched config returned no maximum cap. - if (autoPromptConfig.impressionConfig.maxImpressions === undefined) { - return true; - } - - const [impressions, dismissals] = await Promise.all([ - this.getImpressions_(), - this.getDismissals_(), - ]); - const lastImpression = impressions[impressions.length - 1]; - const lastDismissal = dismissals[dismissals.length - 1]; - - // If the user has reached the maxDismissalsPerWeek, and - // maxDismissalsResultingHideSeconds has not yet passed, do not show a - // soft paywall. - if ( - dismissals.length >= - autoPromptConfig.explicitDismissalConfig.maxDismissalsPerWeek! && - Date.now() - lastDismissal < - (autoPromptConfig.explicitDismissalConfig - .maxDismissalsResultingHideSeconds || 0) * - SECOND_IN_MILLIS - ) { - return false; - } - - // If the user has previously dismissed the prompt, and backOffSeconds has - // not yet passed, do not show a soft paywall. - if ( - autoPromptConfig.explicitDismissalConfig.backOffSeconds && - dismissals.length > 0 && - Date.now() - lastDismissal < - autoPromptConfig.explicitDismissalConfig.backOffSeconds * - SECOND_IN_MILLIS - ) { - return false; - } - - // If the user has reached the maxImpressions, and - // maxImpressionsResultingHideSeconds has not yet passed, do not show a - // soft paywall. - const userReachedMaxImpressions = - impressions.length >= autoPromptConfig.impressionConfig.maxImpressions; - const timeSinceLastImpression = Date.now() - lastImpression; - const timeToWaitAfterMaxImpressionsInSeconds = - autoPromptConfig.impressionConfig.maxImpressionsResultingHideSeconds || 0; - const timeToWaitAfterMaxImpressions = - timeToWaitAfterMaxImpressionsInSeconds * SECOND_IN_MILLIS; - const waitingAfterMaxImpressions = - timeSinceLastImpression < timeToWaitAfterMaxImpressions; - if (userReachedMaxImpressions && waitingAfterMaxImpressions) { - return false; - } - - // If the user has seen the prompt, and backOffSeconds has not yet passed, - // do not show a soft paywall. This is to prevent the prompt from showing - // in consecutive visits. - if ( - autoPromptConfig.impressionConfig.backOffSeconds && - impressions.length > 0 && - Date.now() - lastImpression < - autoPromptConfig.impressionConfig.backOffSeconds * SECOND_IN_MILLIS - ) { - return false; - } - - return true; - } - /** * Determines what Monetization prompt type should be shown. Determined by * the first AutoPromptType passed in from Article Actions. Only enables the @@ -674,75 +349,6 @@ export class AutoPromptManager { return this.getPromptTypeToDisplay_(snippetAction); } - /** - * Determines what action should be used to determine what prompt to show. - * - * In the case of Subscription models, always show the first eligible prompt. - * - * In the case of Contribution models, only show non-previously dismissed - * actions after the initial Contribution prompt. Always default to showing - * the Contribution prompt if permitted by the frequency cap, indicated by - * shouldShowMonetizationPromptAsSoftPaywall. - * - * Has the side effect of setting this.interventionDisplayed_ to an action - * that should be displayed. - */ - private async getAction_({ - article, - autoPromptType, - dismissedPrompts, - canDisplayMonetizationPrompt, - shouldShowMonetizationPromptAsSoftPaywall, - }: { - article: Article; - autoPromptType?: AutoPromptType; - dismissedPrompts?: string | null; - canDisplayMonetizationPrompt: boolean; - shouldShowMonetizationPromptAsSoftPaywall?: boolean; - }): Promise { - const audienceActions = article.audienceActions?.actions || []; - if (shouldShowMonetizationPromptAsSoftPaywall) { - const action = audienceActions.filter((action) => - this.isMonetizationAction_(action.type) - )[0]; - this.interventionDisplayed_ = action; - return action; - } - - const isSurveyEligible = await this.isSurveyEligible_(audienceActions); - let potentialActions = audienceActions.filter((action) => - this.checkActionEligibility_( - action.type, - canDisplayMonetizationPrompt, - isSurveyEligible - ) - ); - - if (!this.isSubscription_(autoPromptType) && !this.pageConfig_.isLocked()) { - // If page is not paywalled, filter out contribution prompt as it meets - // its frequency cap. - potentialActions = potentialActions.filter( - (action) => action.type !== TYPE_CONTRIBUTION - ); - - // Suppress previously dismissed prompts. - if (dismissedPrompts) { - const previouslyShownPrompts = dismissedPrompts.split(','); - potentialActions = potentialActions.filter( - (action) => !previouslyShownPrompts.includes(action.type) - ); - } - } - - if (potentialActions.length === 0) { - return undefined; - } - - const actionToUse = potentialActions[0]; - this.interventionDisplayed_ = actionToUse; - return actionToUse; - } - private async getPotentialAction_({ article, frequencyCapConfig, @@ -755,26 +361,10 @@ export class AutoPromptManager { return; } - let actionsTimestamps; - if (this.frequencyCappingByDismissalsEnabled_) { - actionsTimestamps = await this.getTimestamps(); - actions = actions.filter((action) => - this.checkActionEligibilityFromTimestamps_( - action.type, - actionsTimestamps! - ) - ); - } else { - const isSurveyEligible = await this.isSurveyEligible_(actions); - actions = actions.filter((action) => - this.checkActionEligibility_( - action.type, - // Monetization check does not apply for new frequency capping flow - /** canDisplayMonetizationPrompt */ true, - isSurveyEligible - ) - ); - } + const actionsTimestamps = await this.getTimestamps(); + actions = actions.filter((action) => + this.checkActionEligibility_(action.type, actionsTimestamps!) + ); if (actions.length === 0) { return; @@ -809,16 +399,11 @@ export class AutoPromptManager { frequencyCapConfig?.anyPromptFrequencyCap?.frequencyCapDuration; } if (this.isValidFrequencyCapDuration_(frequencyCapDuration)) { - let timestamps; - if (this.frequencyCappingByDismissalsEnabled_) { - const actionTimestamps = actionsTimestamps![action.type]; - timestamps = [ - ...(actionTimestamps?.dismissals || []), - ...(actionTimestamps?.completions || []), - ]; - } else { - timestamps = await this.getActionImpressions_(action.type); - } + const actionTimestamps = actionsTimestamps![action.type]; + const timestamps = [ + ...(actionTimestamps?.dismissals || []), + ...(actionTimestamps?.completions || []), + ]; if (this.isFrequencyCapped_(frequencyCapDuration!, timestamps)) { this.eventManager_.logSwgEvent( AnalyticsEvent.EVENT_PROMPT_FREQUENCY_CAP_MET @@ -837,17 +422,12 @@ export class AutoPromptManager { const globalFrequencyCapDuration = frequencyCapConfig?.globalFrequencyCap?.frequencyCapDuration; if (this.isValidFrequencyCapDuration_(globalFrequencyCapDuration)) { - let globalTimestamps; - if (this.frequencyCappingByDismissalsEnabled_) { - globalTimestamps = Array.prototype.concat.apply( - [], - Object.entries(actionsTimestamps!) - .filter(([action, _]) => action !== potentialAction!.type) - .map(([_, timestamps]) => timestamps.impressions) - ); - } else { - globalTimestamps = await this.getAllImpressions_(); - } + const globalTimestamps = Array.prototype.concat.apply( + [], + Object.entries(actionsTimestamps!) + .filter(([action, _]) => action !== potentialAction!.type) + .map(([_, timestamps]) => timestamps.impressions) + ); if ( this.isFrequencyCapped_(globalFrequencyCapDuration!, globalTimestamps) ) { @@ -860,7 +440,29 @@ export class AutoPromptManager { return potentialAction; } - private audienceActionPrompt_({ + /** + * Returns a function to show the appropriate monetization prompt, + * or undefined if the type of prompt cannot be determined. + */ + private getLargeMonetizationPromptFn_( + autoPromptType: AutoPromptType | undefined, + isClosable: boolean, + shouldAnimateFade: boolean = true + ): (() => void) | undefined { + const options: OffersRequest = {isClosable, shouldAnimateFade}; + if (this.isSubscription_(autoPromptType)) { + return () => { + this.configuredRuntime_.showOffers(options); + }; + } else if (this.isContribution_(autoPromptType)) { + return () => { + this.configuredRuntime_.showContributionOptions(options); + }; + } + return undefined; + } + + private getAudienceActionPromptFn_({ actionType, configurationId, autoPromptType, @@ -880,7 +482,6 @@ export class AutoPromptManager { action: actionType, configurationId, autoPromptType, - onCancel: this.storeLastDismissal_.bind(this), isClosable, monetizationFunction: this.getLargeMonetizationPromptFn_( autoPromptType, @@ -894,14 +495,12 @@ export class AutoPromptManager { action: actionType, configurationId, autoPromptType, - onCancel: this.storeLastDismissal_.bind(this), isClosable, }) : new AudienceActionIframeFlow(this.deps_, { action: actionType, configurationId, autoPromptType, - onCancel: () => this.storeLastDismissal_(), isClosable, }); this.setLastAudienceActionFlow(audienceActionFlow); @@ -920,25 +519,27 @@ export class AutoPromptManager { /** * Shows the prompt based on the type specified. */ - private showPrompt_( + private getMonetizationPromptFn_( autoPromptType?: AutoPromptType, displayLargePromptFn?: () => void - ): void { - if ( - autoPromptType === AutoPromptType.SUBSCRIPTION || - autoPromptType === AutoPromptType.CONTRIBUTION - ) { - this.miniPromptAPI_.create({ - autoPromptType, - clickCallback: displayLargePromptFn, - }); - } else if ( - (autoPromptType === AutoPromptType.SUBSCRIPTION_LARGE || - autoPromptType === AutoPromptType.CONTRIBUTION_LARGE) && - displayLargePromptFn - ) { - displayLargePromptFn(); - } + ): () => void { + return () => { + if ( + autoPromptType === AutoPromptType.SUBSCRIPTION || + autoPromptType === AutoPromptType.CONTRIBUTION + ) { + this.miniPromptAPI_.create({ + autoPromptType, + clickCallback: displayLargePromptFn, + }); + } else if ( + (autoPromptType === AutoPromptType.SUBSCRIPTION_LARGE || + autoPromptType === AutoPromptType.CONTRIBUTION_LARGE) && + displayLargePromptFn + ) { + displayLargePromptFn(); + } + }; } /** @@ -957,7 +558,7 @@ export class AutoPromptManager { this.doc_.getWin(), ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT ); - const isWideDesktop = this.doc_.getWin()./* OK */ innerWidth > 480; + const isWideDesktop = this.getInnerWidth_() > 480; if (disableDesktopMiniprompt && isWideDesktop) { if (promptType === AutoPromptType.SUBSCRIPTION) { @@ -990,28 +591,6 @@ export class AutoPromptManager { }); } - /** - * Determines whether a larger, blocking prompt should be shown. - */ - private shouldShowBlockingPrompt_(action: Intervention | void): boolean { - const isAudienceAction = - !!action && !this.isMonetizationAction_(action?.type); - return this.pageConfig_.isLocked() || isAudienceAction; - } - - /** - * Determines whether the given prompt type is an action prompt type with display delay. - */ - private isActionPromptWithDelay_( - potentialActionPromptType?: string - ): boolean { - return ( - !this.pageConfig_.isLocked() && - (potentialActionPromptType === TYPE_REWARDED_SURVEY || - potentialActionPromptType === TYPE_REWARDED_AD) - ); - } - /** * Listens for relevant prompt impression events, dismissal events, and completed * action events, and logs them to local storage for use in determining whether @@ -1022,54 +601,11 @@ export class AutoPromptManager { return; } - if (COMPLETED_ACTION_TO_STORAGE_KEY_MAP.has(event.eventType)) { - return this.storage_.storeEvent( - COMPLETED_ACTION_TO_STORAGE_KEY_MAP.get(event.eventType)! - ); - } - // ** Frequency Capping Events ** - if (this.frequencyCappingLocalStorageEnabled_) { - if (ACTON_CTA_BUTTON_CLICK.find((e) => e === event.eventType)) { - this.promptIsFromCtaButton_ = true; - } - if (this.frequencyCappingByDismissalsEnabled_) { - await this.handleFrequencyCappingLocalStorage_(event.eventType); - } else { - await this.handleFrequencyCappingLocalStorageLegacy_(event.eventType); - } - } - - // Impressions and dimissals of forced (for paygated) or manually triggered - // prompts do not count toward the frequency caps. - if ( - !this.monetizationPromptWasDisplayedAsSoftPaywall_ || - this.pageConfig_.isLocked() - ) { - return; - } - - // Prompt impression should be stored if no previous one has been stored. - // This is to prevent the case that user clicks the mini prompt, and both - // impressions of the mini and large prompts would be counted towards the - // cap. - if ( - !this.hasStoredImpression_ && - monetizationImpressionEvents.includes(event.eventType) - ) { - this.hasStoredImpression_ = true; - return this.storage_.storeEvent(StorageKeys.IMPRESSIONS); - } - - if (dismissEvents.includes(event.eventType)) { - await Promise.all([ - this.storage_.storeEvent(StorageKeys.DISMISSALS), - // If we need to keep track of the prompt that was dismissed, make sure to - // record it. - this.storeLastDismissal_(), - ]); - return; + if (ACTON_CTA_BUTTON_CLICK.find((e) => e === event.eventType)) { + this.promptIsFromCtaButton_ = true; } + await this.handleFrequencyCappingLocalStorage_(event.eventType); } /** @@ -1109,100 +645,10 @@ export class AutoPromptManager { this.storeEvent(analyticsEvent); } - private async handleFrequencyCappingLocalStorageLegacy_( - analyticsEvent: AnalyticsEvent - ): Promise { - if ( - !INTERVENTION_TO_STORAGE_KEY_MAP.has(analyticsEvent) || - !this.isClosable_ - ) { - return; - } - - if (monetizationImpressionEvents.includes(analyticsEvent)) { - if (this.hasStoredMiniPromptImpression_) { - return; - } - this.hasStoredMiniPromptImpression_ = true; - } - return this.storage_.storeFrequencyCappingEvent( - INTERVENTION_TO_STORAGE_KEY_MAP.get(analyticsEvent)! - ); - } - - /** - * Adds the current prompt displayed to the array of all dismissed prompts. - */ - private async storeLastDismissal_(): Promise { - if (!this.interventionDisplayed_) { - return; - } - - const value = await this.storage_.get( - StorageKeys.DISMISSED_PROMPTS, - /* useLocalStorage */ true - ); - this.storage_.set( - StorageKeys.DISMISSED_PROMPTS, - value - ? value + ',' + this.interventionDisplayed_.type - : this.interventionDisplayed_.type, - /* useLocalStorage */ true - ); - } - - /** - * Retrieves the locally stored impressions of the auto prompt, within a week - * of the current time. - */ - private getImpressions_(): Promise { - return this.storage_.getEvent(StorageKeys.IMPRESSIONS); - } - - /** - * Retrieves the locally stored dismissals of the auto prompt, within a week - * of the current time. - */ - private getDismissals_(): Promise { - return this.storage_.getEvent(StorageKeys.DISMISSALS); - } - /** - * Fetches impressions timestamp from local storage for all frequency capping - * related prompts and aggregates them into one array. Timestamps are not - * sorted. + * Fetches frequency capping timestamps from local storage for prompts. + * Timestamps are not necessarily sorted. */ - private async getAllImpressions_(): Promise { - const impressions = []; - - for (const storageKey of new Set([ - ...INTERVENTION_TO_STORAGE_KEY_MAP.values(), - ])) { - const promptImpressions = await this.storage_.getFrequencyCappingEvent( - storageKey - ); - impressions.push(...promptImpressions); - } - - return impressions; - } - - /** - * Fetches impression timestamps from local storage for a given action type. - */ - private async getActionImpressions_(actionType: string): Promise { - if (!ACTION_TO_IMPRESSION_STORAGE_KEY_MAP.has(actionType)) { - this.eventManager_.logSwgEvent( - AnalyticsEvent.EVENT_ACTION_IMPRESSIONS_STORAGE_KEY_NOT_FOUND_ERROR - ); - return []; - } - - return this.storage_.getFrequencyCappingEvent( - ACTION_TO_IMPRESSION_STORAGE_KEY_MAP.get(actionType)! - ); - } - async getTimestamps(): Promise { const stringified = await this.storage_.get( StorageKeys.TIMESTAMPS, @@ -1323,6 +769,9 @@ export class AutoPromptManager { } } + private getInnerWidth_(): number { + return this.doc_.getWin()./* OK */ innerWidth; + } /** * Computes if the frequency cap is met from the timestamps of previous * provided by using the maximum/most recent timestamp. @@ -1346,60 +795,10 @@ export class AutoPromptManager { return Math.floor(nano / Math.pow(10, 6)); } - /** - * Checks for survey eligibility, including if survey is present in article - * actions, analytics is setup, and there are no survey completion or survey - * error timestamps. - */ - private async isSurveyEligible_(actions: Intervention[]): Promise { - if (!actions.find((action) => action.type === TYPE_REWARDED_SURVEY)) { - return false; - } - - const isAnalyticsEligible = - GoogleAnalyticsEventListener.isGaEligible(this.deps_) || - GoogleAnalyticsEventListener.isGtagEligible(this.deps_) || - GoogleAnalyticsEventListener.isGtmEligible(this.deps_); - if (!isAnalyticsEligible) { - return false; - } - - const [surveyCompletionTimestamps, surveyDataTransferFailureTimestamps] = - await Promise.all([ - this.storage_.getEvent( - COMPLETED_ACTION_TO_STORAGE_KEY_MAP.get( - AnalyticsEvent.ACTION_SURVEY_DATA_TRANSFER - )! - ), - this.storage_.getEvent(StorageKeys.SURVEY_DATA_TRANSFER_FAILED), - ]); - - const hasCompletedSurveys = surveyCompletionTimestamps.length >= 1; - const hasRecentSurveyDataTransferFailure = - surveyDataTransferFailureTimestamps.length >= 1; - return !hasCompletedSurveys && !hasRecentSurveyDataTransferFailure; - } - /** * Checks AudienceAction eligbility, used to filter potential actions. */ private checkActionEligibility_( - actionType: string, - canDisplayMonetizationPrompt: boolean, - isSurveyEligible: boolean - ): boolean { - if (actionType === TYPE_SUBSCRIPTION || actionType === TYPE_CONTRIBUTION) { - return canDisplayMonetizationPrompt; - } else if (actionType === TYPE_REWARDED_SURVEY) { - return isSurveyEligible; - } - return true; - } - - /** - * Checks AudienceAction eligbility, used to filter potential actions. - */ - private checkActionEligibilityFromTimestamps_( actionType: string, timestamps: ActionsTimestamps ): boolean { @@ -1413,7 +812,7 @@ export class AutoPromptManager { } // Do not show survey if there is a previous completion record. // Client side eligibility is required to handle identity transitions - // after sign-in flow. TODO(justinchou): update survey completion check + // after sign-in flow. TODO(b/332759781): update survey completion check // to persist even after 2 weeks. return !(timestamps[TYPE_REWARDED_SURVEY]?.completions || []).length; } @@ -1439,17 +838,4 @@ export class AutoPromptManager { private isValidFrequencyCapDuration_(duration: Duration | undefined) { return !!duration?.seconds || !!duration?.nano; } - - /** - * Checks if provided ExperimentFlag is enabled within article experiment - * config. - */ - private isArticleExperimentEnabled_( - article: Article, - experimentFlag: string - ): boolean { - const articleExpFlags = - this.entitlementsManager_.parseArticleExperimentConfigFlags(article); - return articleExpFlags.includes(experimentFlag); - } } diff --git a/src/runtime/basic-runtime-test.js b/src/runtime/basic-runtime-test.js index 09bf557174..2e27b291ee 100644 --- a/src/runtime/basic-runtime-test.js +++ b/src/runtime/basic-runtime-test.js @@ -36,6 +36,7 @@ import {Entitlements} from '../api/entitlements'; import {EntitlementsManager} from './entitlements-manager'; import {ExperimentFlags} from './experiment-flags'; import {GlobalDoc} from '../model/doc'; +import {MiniPromptApi} from './mini-prompt-api'; import {MockActivityPort} from '../../test/mock-activity-port'; import {OffersFlow} from './offers-flow'; import {PageConfig} from '../model/page-config'; @@ -673,6 +674,8 @@ describes.realWin('BasicConfiguredRuntime', (env) => { let audienceActivityEventListener; let audienceActivityEventListenerMock; let entitlementsStub; + let miniPromptApiMock; + let autoPromptManagerMock; beforeEach(() => { entitlementsStub = sandbox.stub( @@ -698,12 +701,20 @@ describes.realWin('BasicConfiguredRuntime', (env) => { audienceActivityEventListenerMock = sandbox.mock( audienceActivityEventListener ); + sandbox.stub(MiniPromptApi.prototype, 'init'); + miniPromptApiMock = sandbox.mock( + configuredBasicRuntime.autoPromptManager_.miniPromptAPI_ + ); + autoPromptManagerMock = sandbox.mock( + configuredBasicRuntime.autoPromptManager_ + ); }); afterEach(() => { entitlementsManagerMock.verify(); clientConfigManagerMock.verify(); configuredClassicRuntimeMock.verify(); + miniPromptApiMock.verify(); winMock.verify(); }); @@ -771,9 +782,35 @@ describes.realWin('BasicConfiguredRuntime', (env) => { configuredBasicRuntime.jserror(); }); - it('should configure subscription auto prompts to show offers for paygated content', async () => { + it('should configure subscription miniprompts to show offers for paygated content', async () => { sandbox.stub(pageConfig, 'isLocked').returns(true); + entitlementsManagerMock + .expects('getArticle') + .resolves({ + audienceActions: { + actions: [ + {type: 'TYPE_SUBSCRIPTION', configurationId: 'config_id'}, + ], + engineId: '123', + }, + }) + .atLeast(1); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + clientConfigManagerMock + .expects('getClientConfig') + .resolves({uiPredicates}); + miniPromptApiMock.expects('create').once(); + + await configuredBasicRuntime.setupAndShowAutoPrompt({ + autoPromptType: AutoPromptType.SUBSCRIPTION, + }); + }); + + it('should configure subscription auto prompts to show offers for paygated content when disable desktop miniprompt experiment is enabled', async () => { + setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); + autoPromptManagerMock.expects('getInnerWidth_').returns(500).once(); + sandbox.stub(pageConfig, 'isLocked').returns(true); entitlementsManagerMock .expects('getArticle') .resolves({ @@ -802,9 +839,66 @@ describes.realWin('BasicConfiguredRuntime', (env) => { }); }); - it('should configure contribution auto prompts to show contribution options for paygated content', async () => { + it('should configure subscription auto prompts to show offers for paygated content', async () => { + sandbox.stub(pageConfig, 'isLocked').returns(true); + + entitlementsManagerMock + .expects('getArticle') + .resolves({ + audienceActions: { + actions: [ + {type: 'TYPE_SUBSCRIPTION', configurationId: 'config_id'}, + ], + engineId: '123', + }, + }) + .atLeast(1); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + clientConfigManagerMock + .expects('getClientConfig') + .resolves({uiPredicates}); + configuredClassicRuntimeMock + .expects('showOffers') + .withExactArgs({ + isClosable: false, + shouldAnimateFade: true, + }) + .once(); + + await configuredBasicRuntime.setupAndShowAutoPrompt({ + autoPromptType: AutoPromptType.SUBSCRIPTION_LARGE, + }); + }); + + it('should configure contribution miniprompts to show contribution options for paygated content', async () => { sandbox.stub(pageConfig, 'isLocked').returns(true); + entitlementsManagerMock + .expects('getArticle') + .resolves({ + audienceActions: { + actions: [ + {type: 'TYPE_CONTRIBUTION', configurationId: 'config_id'}, + ], + engineId: '123', + }, + }) + .atLeast(1); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + clientConfigManagerMock + .expects('getClientConfig') + .resolves({uiPredicates}); + miniPromptApiMock.expects('create').once(); + + await configuredBasicRuntime.setupAndShowAutoPrompt({ + autoPromptType: AutoPromptType.CONTRIBUTION, + }); + }); + + it('should configure contribution auto prompts to show contribution options for paygated content when disable desktop miniprompt experiment is enabled', async () => { + setExperiment(win, ExperimentFlags.DISABLE_DESKTOP_MINIPROMPT, true); + autoPromptManagerMock.expects('getInnerWidth_').returns(500).once(); + sandbox.stub(pageConfig, 'isLocked').returns(true); entitlementsManagerMock .expects('getArticle') .resolves({ @@ -833,6 +927,37 @@ describes.realWin('BasicConfiguredRuntime', (env) => { }); }); + it('should configure contribution auto prompts to show contribution options for paygated content', async () => { + sandbox.stub(pageConfig, 'isLocked').returns(true); + + entitlementsManagerMock + .expects('getArticle') + .resolves({ + audienceActions: { + actions: [ + {type: 'TYPE_CONTRIBUTION', configurationId: 'config_id'}, + ], + engineId: '123', + }, + }) + .atLeast(1); + const uiPredicates = new UiPredicates(/* canDisplayAutoPrompt */ true); + clientConfigManagerMock + .expects('getClientConfig') + .resolves({uiPredicates}); + configuredClassicRuntimeMock + .expects('showContributionOptions') + .withExactArgs({ + isClosable: true, + shouldAnimateFade: true, + }) + .once(); + + await configuredBasicRuntime.setupAndShowAutoPrompt({ + autoPromptType: AutoPromptType.CONTRIBUTION_LARGE, + }); + }); + it('should dimiss SwG UI', () => { const dialogManagerMock = sandbox.mock( configuredBasicRuntime.dialogManager() diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 79884ed2a4..cc8836e89f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -33,16 +33,6 @@ const Constants = { }; const StorageKeys = { - /** - * Local storage key for autoprompt dismissal timestamps. - */ - DISMISSALS: 'autopromptdismiss', - - /** - * Local storage key for dismissed prompts. - */ - DISMISSED_PROMPTS: 'dismissedprompts', - /** * Local storage key for cacheable entitlements. */ @@ -54,11 +44,6 @@ const StorageKeys = { */ PPS_TAXONOMIES: 'ppstaxonomies', - /** - * Local storage key for autoprompt impression timestamps. - */ - IMPRESSIONS: 'autopromptimp', - /** * Local storage key for whether credential isReadyToPay. */ @@ -84,40 +69,10 @@ const StorageKeys = { */ TOAST: 'toast', - // Prompt Frequency storage keys - /** - * Local storage key for prompt impression timestamps. - */ - PF_IMPRESSIONS: 'imp', - /** - * Local storage key for prompt dismissal timestamps. + * Local storage key for frequency capping timestamps. */ - PF_DISMISSALS: 'dms', - - /** - * Local storage key for prompt completion timestamps. - */ - PF_COMPLETIONS: 'cpt', - TIMESTAMPS: 'tsp', }; -/** - * Local storage keys for intervention impressions. - */ -const ImpressionStorageKeys = { - CONTRIBUTION: 'imp_contribution', - - NEWSLETTER_SIGNUP: 'imp_newsletter', - - REGISTRATION_WALL: 'imp_regwall', - - REWARDED_AD: 'imp_ad', - - REWARDED_SURVEY: 'imp_survey', - - SUBSCRIPTION: 'imp_subscription', -}; - -export {Constants, ImpressionStorageKeys, StorageKeys}; +export {Constants, StorageKeys};