diff --git a/assets/js/modules/reader-revenue-manager/datastore/constants.js b/assets/js/modules/reader-revenue-manager/datastore/constants.js index e39279b277d..6bb96f07935 100644 --- a/assets/js/modules/reader-revenue-manager/datastore/constants.js +++ b/assets/js/modules/reader-revenue-manager/datastore/constants.js @@ -16,12 +16,18 @@ * limitations under the License. */ +export const ERROR_CODE_NON_HTTPS_SITE = 'non_https_site'; + export const MODULES_READER_REVENUE_MANAGER = 'modules/reader-revenue-manager'; -export const ERROR_CODE_NON_HTTPS_SITE = 'non_https_site'; +export const MODULE_SLUG = 'reader-revenue-manager'; export const PUBLICATION_ONBOARDING_STATES = { ONBOARDING_COMPLETE: 'ONBOARDING_COMPLETE', ONBOARDING_ACTION_REQUIRED: 'ONBOARDING_ACTION_REQUIRED', PENDING_VERIFICATION: 'PENDING_VERIFICATION', + UNSPECIFIED: 'ONBOARDING_STATE_UNSPECIFIED', }; + +export const UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION = + 'READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION'; diff --git a/assets/js/modules/reader-revenue-manager/datastore/publications.js b/assets/js/modules/reader-revenue-manager/datastore/publications.js index e20dd74bb3a..946dc6e29b8 100644 --- a/assets/js/modules/reader-revenue-manager/datastore/publications.js +++ b/assets/js/modules/reader-revenue-manager/datastore/publications.js @@ -17,14 +17,18 @@ */ /** - * Internal dependencies + * Internal dependencies. */ import API from 'googlesitekit-api'; +import { CORE_MODULES } from '../../../googlesitekit/modules/datastore/constants'; +import { CORE_UI } from '../../../googlesitekit/datastore/ui/constants'; import { commonActions, combineStores } from 'googlesitekit-data'; import { createFetchStore } from '../../../googlesitekit/data/create-fetch-store'; import { MODULES_READER_REVENUE_MANAGER, + MODULE_SLUG, PUBLICATION_ONBOARDING_STATES, + UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION, } from './constants'; const fetchGetPublicationsStore = createFetchStore( { @@ -32,7 +36,7 @@ const fetchGetPublicationsStore = createFetchStore( { controlCallback: () => API.get( 'modules', - 'reader-revenue-manager', + MODULE_SLUG, 'publications', {}, { useCache: true } @@ -45,6 +49,98 @@ const baseInitialState = { }; const baseActions = { + /** + * Syncronizes the onboarding state of the publication with the API. + * Updates the settings on the server. + * + * @since n.e.x.t + * + * @return {void} + */ + *syncPublicationOnboardingState() { + const registry = yield commonActions.getRegistry(); + const connected = yield commonActions.await( + registry + .resolveSelect( CORE_MODULES ) + .isModuleConnected( MODULE_SLUG ) + ); + + // If the module is not connected, do not attempt to sync the onboarding state. + if ( ! connected ) { + return; + } + + yield commonActions.await( + registry + .resolveSelect( MODULES_READER_REVENUE_MANAGER ) + .getSettings() + ); + + const publicationID = registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublicationID(); + + // If there is no publication ID, do not attempt to sync the onboarding state. + if ( publicationID === undefined ) { + return; + } + + const publications = registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublications(); + + // If there are no publications, do not attempt to sync the onboarding state. + if ( ! publications ) { + return; + } + + const publication = publications.find( + // eslint-disable-next-line sitekit/acronym-case + ( { publicationId } ) => publicationId === publicationID + ); + + // If the publication is not found, do not attempt to sync the onboarding state. + if ( ! publication ) { + return; + } + + const { onboardingState } = publication; + const currentOnboardingState = registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublicationOnboardingState(); + + const settings = registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getSettings(); + + if ( onboardingState !== currentOnboardingState ) { + settings.publicationOnboardingState = onboardingState; + } + + // eslint-disable-next-line sitekit/no-direct-date + settings.publicationOnboardingStateLastSyncedAtMs = Date.now(); + + registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .setSettings( settings ); + + // Save the settings to the API. + registry.dispatch( MODULES_READER_REVENUE_MANAGER ).saveSettings(); + + // If the onboarding state is complete, set the key in CORE_UI to trigger the notification. + if ( + onboardingState === + PUBLICATION_ONBOARDING_STATES.ONBOARDING_COMPLETE + ) { + registry + .dispatch( CORE_UI ) + .setValue( + UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION, + true + ); + } + }, + /** * Finds a matched publication. * @@ -94,6 +190,7 @@ const baseResolvers = { .getPublications(); if ( publications === undefined ) { yield fetchGetPublicationsStore.actions.fetchGetPublications(); + yield baseActions.syncPublicationOnboardingState(); } }, }; diff --git a/assets/js/modules/reader-revenue-manager/datastore/publications.test.js b/assets/js/modules/reader-revenue-manager/datastore/publications.test.js index bcbeda71332..aa1c4ae8d8f 100644 --- a/assets/js/modules/reader-revenue-manager/datastore/publications.test.js +++ b/assets/js/modules/reader-revenue-manager/datastore/publications.test.js @@ -1,3 +1,4 @@ +/* eslint-disable sitekit/acronym-case */ /** * `modules/reader-revenue-manager` data store: publications tests. * @@ -16,6 +17,11 @@ * limitations under the License. */ +/** + * External dependencies + */ +import fetchMock from 'fetch-mock'; + /** * Internal dependencies */ @@ -23,108 +29,196 @@ import API from 'googlesitekit-api'; import { createTestRegistry, untilResolved, + provideModules, + provideUserInfo, + provideModuleRegistrations, } from '../../../../../tests/js/utils'; import * as fixtures from './__fixtures__'; +import { enabledFeatures } from '../../../features'; +import { CORE_UI } from '../../../googlesitekit/datastore/ui/constants'; import { MODULES_READER_REVENUE_MANAGER, + MODULE_SLUG, PUBLICATION_ONBOARDING_STATES, + UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION, } from './constants'; describe( 'modules/reader-revenue-manager publications', () => { let registry; + const getModulesEndpoint = new RegExp( + '^/google-site-kit/v1/core/modules/data/list' + ); + const publicationsEndpoint = new RegExp( '^/google-site-kit/v1/modules/reader-revenue-manager/data/publications' ); + const settingsEndpoint = new RegExp( + '^/google-site-kit/v1/modules/reader-revenue-manager/data/settings' + ); + beforeAll( () => { API.setUsingCache( false ); } ); beforeEach( () => { + enabledFeatures.add( 'rrmModule' ); // Enable RRM module to get its features. registry = createTestRegistry(); + provideUserInfo( registry ); } ); - describe( 'selectors', () => { - describe( 'getPublications', () => { - it( 'should use a resolver to make a network request', async () => { - fetchMock.get( publicationsEndpoint, { - body: fixtures.publications, - status: 200, - } ); + describe( 'actions', () => { + beforeEach( () => { + // Make sure the RRM module is active and connected. + const extraData = [ + { + slug: MODULE_SLUG, + active: true, + connected: true, + }, + ]; + provideModules( registry, extraData ); + provideModuleRegistrations( registry, extraData ); + } ); - const initialPublications = registry - .select( MODULES_READER_REVENUE_MANAGER ) - .getPublications(); - expect( initialPublications ).toBeUndefined(); + describe( 'syncPublicationOnboardingState', () => { + it( 'should return undefined when no publication ID is present', async () => { + registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .receiveGetPublications( fixtures.publications ); - await untilResolved( - registry, - MODULES_READER_REVENUE_MANAGER - ).getPublications(); - expect( fetchMock ).toHaveFetched( publicationsEndpoint ); + registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .receiveGetSettings( {} ); - const publications = registry - .select( MODULES_READER_REVENUE_MANAGER ) - .getPublications(); - expect( fetchMock ).toHaveFetchedTimes( 1 ); - expect( publications ).toEqual( fixtures.publications ); - expect( publications ).toHaveLength( - fixtures.publications.length - ); + const syncStatus = await registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .syncPublicationOnboardingState(); + + expect( syncStatus ).toBeUndefined(); } ); - it( 'should not make a network request if publications are already present', async () => { + it( 'should update the settings and call the saveSettings endpoint', async () => { + const originalDateNow = Date.now; + + // Mock the date to be an arbitrary time. + const mockNow = new Date( '2020-01-01 12:30:00' ).getTime(); + Date.now = jest.fn( () => mockNow ); + registry .dispatch( MODULES_READER_REVENUE_MANAGER ) .receiveGetPublications( fixtures.publications ); - const publications = registry + const publication = fixtures.publications[ 0 ]; + + const settings = { + publicationID: publication.publicationId, + publicationOnboardingState: + PUBLICATION_ONBOARDING_STATES.PENDING_VERIFICATION, + publicationOnboardingStateLastSyncedAtMs: 0, + }; + + fetchMock.postOnce( settingsEndpoint, { + body: { + ...settings, + publicationOnboardingState: + PUBLICATION_ONBOARDING_STATES.UNSPECIFIED, + publicationOnboardingStateLastSyncedAtMs: mockNow, + }, + status: 200, + } ); + + registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .receiveGetSettings( settings ); + + await registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .syncPublicationOnboardingState(); + + // Set expectations for settings endpoint. + expect( fetchMock ).toHaveFetched( settingsEndpoint ); + expect( fetchMock ).toHaveFetchedTimes( 1 ); + + // Set expectations for publication ID. + expect( + registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublicationID() + ).toEqual( 'ABCDEFGH' ); + + // Set expectations for publication onboarding state. + expect( + registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublicationOnboardingState() + ).toEqual( PUBLICATION_ONBOARDING_STATES.UNSPECIFIED ); + + const syncTimeMs = registry .select( MODULES_READER_REVENUE_MANAGER ) - .getPublications(); - await untilResolved( - registry, - MODULES_READER_REVENUE_MANAGER - ).getPublications(); + .getPublicationOnboardingStateLastSyncedAtMs(); - expect( fetchMock ).not.toHaveFetched( publicationsEndpoint ); - expect( publications ).toEqual( fixtures.publications ); - expect( publications ).toHaveLength( - fixtures.publications.length - ); + // Restore Date.now method. + Date.now = originalDateNow; + + // Ensure that the sync time is set. + expect( syncTimeMs ).not.toBe( 0 ); + expect( syncTimeMs ).toBe( mockNow ); } ); - it( 'should dispatch an error if the request fails', async () => { - const response = { - code: 'internal_server_error', - message: 'Internal server error', - data: { status: 500 }, + it( 'should set UI_KEY_SHOW_RRM_PUBLICATION_APPROVED_NOTIFICATION to true in CORE_UI store', async () => { + registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .receiveGetPublications( fixtures.publications ); + + const publication = fixtures.publications[ 3 ]; + + // Set the current settings. + const settings = { + publicationID: publication.publicationId, + publicationOnboardingState: + PUBLICATION_ONBOARDING_STATES.UNSPECIFIED, + publicationOnboardingStateLastSyncedAtMs: 0, }; - fetchMock.getOnce( publicationsEndpoint, { - body: response, - status: 500, + fetchMock.postOnce( settingsEndpoint, { + body: { + ...settings, + publicationOnboardingState: + PUBLICATION_ONBOARDING_STATES.ONBOARDING_COMPLETE, + publicationOnboardingStateLastSyncedAtMs: Date.now(), // This is set purely for illustrative purposes, the actual value will be calculated at the point of dispatch. + }, + status: 200, } ); registry - .select( MODULES_READER_REVENUE_MANAGER ) - .getPublications(); - await untilResolved( - registry, - MODULES_READER_REVENUE_MANAGER - ).getPublications(); - expect( fetchMock ).toHaveFetchedTimes( 1 ); + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .receiveGetSettings( settings ); - const publications = registry - .select( MODULES_READER_REVENUE_MANAGER ) - .getPublications(); - expect( publications ).toBeUndefined(); - expect( console ).toHaveErrored(); + expect( + registry + .select( CORE_UI ) + .getValue( + UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION + ) + ).toBeUndefined(); + + await registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .syncPublicationOnboardingState(); + + // Ensure that the UI key is set to true. + expect( + registry + .select( CORE_UI ) + .getValue( + UI_KEY_READER_REVENUE_MANAGER_SHOW_PUBLICATION_APPROVED_NOTIFICATION + ) + ).toBe( true ); } ); } ); - } ); - describe( 'actions', () => { describe( 'findMatchedPublication', () => { it( 'should return null if there are no publications', async () => { registry @@ -190,4 +284,93 @@ describe( 'modules/reader-revenue-manager publications', () => { } ); } ); } ); + + describe( 'selectors', () => { + beforeEach( () => { + provideModules( registry ); + provideModuleRegistrations( registry ); + } ); + + describe( 'getPublications', () => { + it( 'should use a resolver to make a network request', async () => { + fetchMock.get( publicationsEndpoint, { + body: fixtures.publications, + status: 200, + } ); + + fetchMock.get( getModulesEndpoint, { + body: undefined, + status: 200, + } ); + + const initialPublications = registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublications(); + expect( initialPublications ).toBeUndefined(); + + await untilResolved( + registry, + MODULES_READER_REVENUE_MANAGER + ).getPublications(); + expect( fetchMock ).toHaveFetched( publicationsEndpoint ); + + const publications = registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublications(); + expect( fetchMock ).toHaveFetchedTimes( 1 ); + expect( publications ).toEqual( fixtures.publications ); + expect( publications ).toHaveLength( + fixtures.publications.length + ); + } ); + + it( 'should not make a network request if publications are already present', async () => { + registry + .dispatch( MODULES_READER_REVENUE_MANAGER ) + .receiveGetPublications( fixtures.publications ); + + const publications = registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublications(); + await untilResolved( + registry, + MODULES_READER_REVENUE_MANAGER + ).getPublications(); + + expect( fetchMock ).not.toHaveFetched( publicationsEndpoint ); + expect( publications ).toEqual( fixtures.publications ); + expect( publications ).toHaveLength( + fixtures.publications.length + ); + } ); + + it( 'should dispatch an error if the request fails', async () => { + const response = { + code: 'internal_server_error', + message: 'Internal server error', + data: { status: 500 }, + }; + + fetchMock.getOnce( publicationsEndpoint, { + body: response, + status: 500, + } ); + + registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublications(); + await untilResolved( + registry, + MODULES_READER_REVENUE_MANAGER + ).getPublications(); + expect( fetchMock ).toHaveFetchedTimes( 1 ); + + const publications = registry + .select( MODULES_READER_REVENUE_MANAGER ) + .getPublications(); + expect( publications ).toBeUndefined(); + expect( console ).toHaveErrored(); + } ); + } ); + } ); } );