diff --git a/assets/images/analytics-banner-bg.png b/assets/images/analytics-banner-bg.png
new file mode 100644
index 000000000000..6b7a8a17cb9a
Binary files /dev/null and b/assets/images/analytics-banner-bg.png differ
diff --git a/assets/src/dashboard/app/index.js b/assets/src/dashboard/app/index.js
index 684aa6ea3938..37724f5be649 100644
--- a/assets/src/dashboard/app/index.js
+++ b/assets/src/dashboard/app/index.js
@@ -49,7 +49,7 @@ import {
} from '../components';
import ApiProvider from './api/apiProvider';
import { Route, RouterProvider, matchPath, useRouteHistory } from './router';
-import { ConfigProvider, useConfig } from './config';
+import { ConfigProvider } from './config';
import {
EditorSettingsView,
ExploreTemplatesView,
@@ -65,9 +65,7 @@ const AppContent = () => {
state: { currentPath },
} = useRouteHistory();
- const { capabilities: { canManageSettings } = {} } = useConfig();
- const enableSettingsView =
- useFeature('enableSettingsView') && canManageSettings;
+ const enableSettingsView = useFeature('enableSettingsView');
useEffect(() => {
const dynamicPageTitle = ROUTE_TITLES[currentPath] || ROUTE_TITLES.DEFAULT;
diff --git a/assets/src/dashboard/app/views/editorSettings/index.js b/assets/src/dashboard/app/views/editorSettings/index.js
index ea029f92c1f8..2504a09e6cc6 100644
--- a/assets/src/dashboard/app/views/editorSettings/index.js
+++ b/assets/src/dashboard/app/views/editorSettings/index.js
@@ -36,6 +36,7 @@ import {
} from '../../../constants';
import { useConfig } from '../../config';
import { PageHeading } from '../shared';
+import useTelemetryOptIn from '../shared/useTelemetryOptIn';
import GoogleAnalyticsSettings from './googleAnalytics';
import { Main, Wrapper } from './components';
import PublisherLogoSettings from './publisherLogo';
@@ -45,11 +46,8 @@ const ACTIVE_DIALOG_REMOVE_LOGO = 'REMOVE_LOGO';
function EditorSettings() {
const {
- currentUser,
- fetchCurrentUser,
fetchSettings,
updateSettings,
- toggleWebStoriesTrackingOptIn,
googleAnalyticsId,
fetchMediaById,
uploadMedia,
@@ -63,7 +61,6 @@ function EditorSettings() {
actions: {
settingsApi: { fetchSettings, updateSettings },
mediaApi: { fetchMediaById, uploadMedia },
- usersApi: { fetchCurrentUser, toggleWebStoriesTrackingOptIn },
},
state: {
settings: {
@@ -72,7 +69,6 @@ function EditorSettings() {
publisherLogoIds,
},
media: { isLoading: isMediaLoading, mediaById, newlyCreatedMediaIds },
- currentUser,
},
}) => ({
fetchSettings,
@@ -85,9 +81,6 @@ function EditorSettings() {
mediaById,
newlyCreatedMediaIds,
publisherLogoIds,
- fetchCurrentUser,
- toggleWebStoriesTrackingOptIn,
- currentUser,
})
);
@@ -97,6 +90,12 @@ function EditorSettings() {
maxUploadFormatted,
} = useConfig();
+ const {
+ disabled,
+ toggleWebStoriesTrackingOptIn,
+ optedIn,
+ } = useTelemetryOptIn();
+
const [activeDialog, setActiveDialog] = useState(null);
const [activeLogo, setActiveLogo] = useState('');
const [mediaError, setMediaError] = useState('');
@@ -113,9 +112,10 @@ function EditorSettings() {
*/
useEffect(() => {
- fetchSettings();
- fetchCurrentUser();
- }, [fetchCurrentUser, fetchSettings]);
+ if (canManageSettings) {
+ fetchSettings();
+ }
+ }, [fetchSettings, canManageSettings]);
useEffect(() => {
if (newlyCreatedMediaIds.length > 0) {
@@ -330,11 +330,9 @@ function EditorSettings() {
/>
)}
diff --git a/assets/src/dashboard/app/views/exploreTemplates/header/test/header.js b/assets/src/dashboard/app/views/exploreTemplates/header/test/header.js
index eeee0b49a0ac..7e45e396d3ef 100644
--- a/assets/src/dashboard/app/views/exploreTemplates/header/test/header.js
+++ b/assets/src/dashboard/app/views/exploreTemplates/header/test/header.js
@@ -27,7 +27,7 @@ import {
VIEW_STYLE,
TEMPLATES_GALLERY_SORT_OPTIONS,
} from '../../../../../constants';
-import { renderWithThemeAndFlagsProvider } from '../../../../../testUtils';
+import { renderWithProviders } from '../../../../../testUtils';
import LayoutProvider from '../../../../../components/layout/provider';
import Header from '../';
@@ -69,7 +69,7 @@ const fakeTemplates = [
describe('Explore Templates ', function () {
it('should have results label that says "Viewing all templates" on initial page view', function () {
- const { getByText } = renderWithThemeAndFlagsProvider(
+ const { getByText } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressTemplateActions: false }
+ { features: { enableInProgressTemplateActions: false } }
);
expect(getByText('Viewing all templates')).toBeInTheDocument();
});
it('should render with the correct count label and search keyword.', function () {
- const { getByPlaceholderText, getByText } = renderWithThemeAndFlagsProvider(
+ const { getByPlaceholderText, getByText } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressTemplateActions: true }
+ { features: { enableInProgressTemplateActions: true } }
);
expect(getByPlaceholderText('Search Templates').value).toBe('Harry Potter');
expect(getByText('8 results')).toBeInTheDocument();
@@ -118,7 +118,7 @@ describe('Explore Templates ', function () {
it('should call the set keyword function when new text is searched', function () {
const setKeywordFn = jest.fn();
- const { getByPlaceholderText } = renderWithThemeAndFlagsProvider(
+ const { getByPlaceholderText } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressTemplateActions: true }
+ { features: { enableInProgressTemplateActions: true } }
);
fireEvent.change(getByPlaceholderText('Search Templates'), {
target: { value: 'Hermione Granger' },
@@ -145,7 +145,7 @@ describe('Explore Templates ', function () {
it('should call the set sort function when a new sort is selected', function () {
const setSortFn = jest.fn();
- const { getAllByText, getByText } = renderWithThemeAndFlagsProvider(
+ const { getAllByText, getByText } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressTemplateActions: true }
+ { features: { enableInProgressTemplateActions: true } }
);
fireEvent.click(getAllByText('Popular')[0].parentElement);
fireEvent.click(getByText('Recent'));
@@ -170,8 +170,8 @@ describe('Explore Templates ', function () {
expect(setSortFn).toHaveBeenCalledWith('recent');
});
- it('should not render with search when enableInProgressTemplateActions is false', function () {
- const { queryAllByRole } = renderWithThemeAndFlagsProvider(
+ it('should not render with search when features:{enableInProgressTemplateActions is false}', function () {
+ const { queryAllByRole } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressTemplateActions: false }
+ { features: { enableInProgressTemplateActions: false } }
);
expect(queryAllByRole('textbox')).toHaveLength(0);
});
diff --git a/assets/src/dashboard/app/views/myStories/header/test/header.js b/assets/src/dashboard/app/views/myStories/header/test/header.js
index 9deb9e62141b..7f4cc74ebf25 100644
--- a/assets/src/dashboard/app/views/myStories/header/test/header.js
+++ b/assets/src/dashboard/app/views/myStories/header/test/header.js
@@ -29,7 +29,7 @@ import {
STORY_STATUSES,
} from '../../../../../constants';
import LayoutProvider from '../../../../../components/layout/provider';
-import { renderWithTheme } from '../../../../../testUtils';
+import { renderWithProviders } from '../../../../../testUtils';
import Header from '../';
const fakeStories = [
@@ -64,7 +64,7 @@ const fakeStories = [
describe('My Stories ', function () {
it('should have results label that says "Viewing all stories" on initial page view', function () {
- const { getByText } = renderWithTheme(
+ const { getByText } = renderWithProviders(
', function () {
});
it('should render with the correct count label and search keyword.', function () {
- const { getByPlaceholderText, getByText } = renderWithTheme(
+ const { getByPlaceholderText, getByText } = renderWithProviders(
', function () {
});
it('should have results label that says "Viewing drafts" when filter is set to drafts', function () {
- const { getByText } = renderWithTheme(
+ const { getByText } = renderWithProviders(
', function () {
});
it('should have 3 toggle buttons, one for each status that say how many items belong to that status', function () {
- const { getByText } = renderWithTheme(
+ const { getByText } = renderWithProviders(
', function () {
it('should call the set keyword function when new text is searched', async function () {
const setKeywordFn = jest.fn();
- const { getByPlaceholderText } = renderWithTheme(
+ const { getByPlaceholderText } = renderWithProviders(
', function () {
it('should call the set sort function when a new sort is selected', function () {
const setSortFn = jest.fn();
- const { getAllByText, getByText } = renderWithTheme(
+ const { getAllByText, getByText } = renderWithProviders(
', function () {
});
it('should render with the correct count label and search keyword.', function () {
- const { getByPlaceholderText, getByText } = renderWithThemeAndFlagsProvider(
+ const { getByPlaceholderText, getByText } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressStoryActions: false }
+ { features: { enableInProgressStoryActions: false } }
);
expect(getByPlaceholderText('Search Templates').value).toBe('Harry Potter');
expect(getByText('3 results')).toBeInTheDocument();
@@ -89,7 +89,7 @@ describe('', function () {
it('should call the set keyword function when new text is searched', function () {
const setKeywordFn = jest.fn();
- const { getByPlaceholderText } = renderWithThemeAndFlagsProvider(
+ const { getByPlaceholderText } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressStoryActions: false }
+ { features: { enableInProgressStoryActions: false } }
);
fireEvent.change(getByPlaceholderText('Search Templates'), {
target: { value: 'Hermione Granger' },
@@ -112,7 +112,7 @@ describe('', function () {
it('should call the set sort function when a new sort is selected', function () {
const setSortFn = jest.fn();
- const { getAllByText, getByText } = renderWithThemeAndFlagsProvider(
+ const { getAllByText, getByText } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressStoryActions: false }
+ { features: { enableInProgressStoryActions: false } }
);
fireEvent.click(getAllByText('Created by')[0].parentElement);
fireEvent.click(getByText('Last modified'));
@@ -134,7 +134,7 @@ describe('', function () {
});
it('should render the content grid with the correct story count.', function () {
- const { getAllByText } = renderWithThemeAndFlagsProvider(
+ const { getAllByText } = renderWithProviders(
', function () {
}}
/>
,
- { enableInProgressStoryActions: false }
+ { features: { enableInProgressStoryActions: false } }
);
expect(getAllByText('Use template')).toHaveLength(fakeStories.length);
diff --git a/assets/src/dashboard/app/views/shared/karma/telemetryBanner.karma.js b/assets/src/dashboard/app/views/shared/karma/telemetryBanner.karma.js
new file mode 100644
index 000000000000..25db47cdebfe
--- /dev/null
+++ b/assets/src/dashboard/app/views/shared/karma/telemetryBanner.karma.js
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Internal dependencies
+ */
+import Fixture from '../../../../karma/fixture';
+import useApi from '../../../api/useApi';
+
+describe('Telemetry Banner', () => {
+ let fixture;
+
+ beforeEach(async () => {
+ fixture = new Fixture();
+ fixture.setFlags({ enableSettingsView: true });
+
+ await fixture.render();
+
+ // Set the initial value of optIn to be false (it's default is true in the api fixture)
+ const { toggleWebStoriesTrackingOptIn } = await fixture.renderHook(() =>
+ useApi(
+ ({
+ actions: {
+ usersApi: { toggleWebStoriesTrackingOptIn },
+ },
+ }) => ({ toggleWebStoriesTrackingOptIn })
+ )
+ );
+
+ await fixture.act(() => toggleWebStoriesTrackingOptIn());
+ });
+
+ afterEach(() => {
+ fixture.restore();
+ });
+
+ it('should render the telemetry opt in banner', async () => {
+ const bannerHeader = await fixture.screen.getByText(
+ /Help improve the editor!/
+ );
+
+ expect(bannerHeader).toBeTruthy();
+ });
+
+ it('should close the banner when the exit button is closed', async () => {
+ const exitButton = await fixture.screen.getByRole('button', {
+ name: /Dismiss Notice/,
+ });
+
+ await fixture.events.click(exitButton);
+
+ const bannerHeader = await fixture.screen.queryByText(
+ /Help improve the editor!/
+ );
+
+ expect(bannerHeader).toBeNull();
+ });
+
+ it('should enable telemetry tracking when the checkbox is clicked', async () => {
+ let optedIn = await fixture.renderHook(() =>
+ useApi(
+ ({ state: { currentUser } }) =>
+ currentUser.data?.meta?.web_stories_tracking_optin ?? false
+ )
+ );
+
+ expect(optedIn).toBeFalse();
+
+ const checkbox = await fixture.querySelector('#telemetry-banner-opt-in');
+
+ expect(checkbox).toBeTruthy();
+
+ await fixture.events.click(checkbox);
+
+ optedIn = await fixture.renderHook(() =>
+ useApi(
+ ({ state: { currentUser } }) =>
+ currentUser.data?.meta?.web_stories_tracking_optin ?? false
+ )
+ );
+
+ expect(optedIn).toBeTrue();
+
+ const bannerHeader = await fixture.screen.getByText(
+ /Your selection has been updated./
+ );
+
+ expect(bannerHeader).toBeTruthy();
+ });
+
+ it('should not display the banner after it has been closed with', async () => {
+ const exitButton = await fixture.screen.getByRole('button', {
+ name: /Dismiss Notice/,
+ });
+
+ await fixture.events.click(exitButton);
+
+ let bannerHeader = await fixture.screen.queryByText(
+ /Help improve the editor!/
+ );
+
+ expect(bannerHeader).toBeNull();
+
+ await fixture.render();
+
+ bannerHeader = await fixture.screen.queryByText(/Help improve the editor!/);
+
+ expect(bannerHeader).toBeNull();
+ });
+});
diff --git a/assets/src/dashboard/app/views/shared/pageHeading.js b/assets/src/dashboard/app/views/shared/pageHeading.js
index 0f4f2b5f5a41..dc3831b1db23 100644
--- a/assets/src/dashboard/app/views/shared/pageHeading.js
+++ b/assets/src/dashboard/app/views/shared/pageHeading.js
@@ -31,6 +31,7 @@ import {
StandardViewContentGutter,
} from '../../../components';
import TypeaheadSearch from './typeaheadSearch';
+import TelemetryBanner from './telemetryBanner';
const Container = styled.div`
padding: 10px 0 0;
@@ -107,6 +108,7 @@ const PageHeading = ({
}) => {
return (
+
diff --git a/assets/src/dashboard/app/views/shared/stories/telemetryBanner.js b/assets/src/dashboard/app/views/shared/stories/telemetryBanner.js
new file mode 100644
index 000000000000..35f98867c3a7
--- /dev/null
+++ b/assets/src/dashboard/app/views/shared/stories/telemetryBanner.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Internal dependencies
+ */
+/**
+ * External dependencies
+ */
+import { boolean } from '@storybook/addon-knobs';
+import { action } from '@storybook/addon-actions';
+import { TelemetryOptInBanner } from '../telemetryBanner';
+
+export default {
+ title: 'Dashboard/Views/Shared/TelemetryBanner',
+ component: TelemetryOptInBanner,
+};
+
+export const _default = () => {
+ return (
+
+ );
+};
diff --git a/assets/src/dashboard/app/views/shared/telemetryBanner.js b/assets/src/dashboard/app/views/shared/telemetryBanner.js
new file mode 100644
index 000000000000..0913c36e9cac
--- /dev/null
+++ b/assets/src/dashboard/app/views/shared/telemetryBanner.js
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * External dependencies
+ */
+import styled from 'styled-components';
+import PropTypes from 'prop-types';
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+/**
+ * Internal dependencies
+ */
+import { TypographyPresets } from '../../../components';
+import { Close as CloseSVG } from '../../../icons';
+import { ICON_METRICS } from '../../../constants';
+import { useConfig } from '../../config';
+import useTelemetryOptIn from './useTelemetryOptIn';
+
+const Banner = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 161px;
+ margin: 0 20px;
+ padding-top: 24px;
+ background-image: url('${({ $backgroundUrl }) => $backgroundUrl}');
+ border-radius: 8px;
+`;
+
+const Header = styled.div`
+ width: 100%;
+ text-align: center;
+`;
+
+const Title = styled.h1`
+ display: inline-block;
+ font-size: 18px;
+ font-weight: ${({ theme }) => theme.typography.weight.normal};
+ line-height: 24px;
+ margin-bottom: 13px;
+`;
+
+const Label = styled.label.attrs({ htmlFor: 'telemetry-banner-opt-in' })`
+ display: flex;
+`;
+
+export const LabelText = styled.span`
+ ${TypographyPresets.Small};
+ color: ${({ theme }) => theme.colors.gray400};
+ margin-bottom: 16px;
+ max-width: 530px;
+`;
+
+const VisitSettingsText = styled(LabelText)``;
+
+const CheckBox = styled.input.attrs({
+ type: 'checkbox',
+ id: 'telemetry-banner-opt-in',
+})`
+ height: 18px;
+ width: 18px;
+ margin: 5px 12px 0 0;
+`;
+
+const CloseIcon = styled(CloseSVG).attrs(ICON_METRICS.TELEMETRY_BANNER_EXIT)`
+ color: ${({ theme }) => theme.colors.white};
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+`;
+
+const ToggleButton = styled.button.attrs({
+ ['aria-label']: __('Dismiss Notice', 'web-stories'),
+})`
+ border: none;
+ padding: 4px;
+ border-radius: 50%;
+ background: ${({ theme }) => theme.colors.gray200};
+ cursor: pointer;
+ float: right;
+ margin-right: 14px;
+
+ &:hover svg {
+ color: ${({ theme }) => theme.colors.gray50};
+ }
+ &:active svg {
+ color: ${({ theme }) => theme.colors.gray50};
+ }
+`;
+
+export function TelemetryOptInBanner({
+ visible = true,
+ disabled = false,
+ onChange = () => {},
+ onClose = () => {},
+ checked = false,
+}) {
+ const { assetsURL } = useConfig();
+
+ return visible ? (
+
+
+
+ {checked
+ ? __(
+ 'Your selection has been updated. Thank you for helping to improve the editor!',
+ 'web-stories'
+ )
+ : __('Help improve the editor!', 'web-stories')}
+
+
+
+
+
+
+
+ {__(
+ 'You can update your selection later by visiting Settings.',
+ 'web-stories'
+ )}
+
+
+ ) : null;
+}
+
+TelemetryOptInBanner.propTypes = {
+ visible: PropTypes.bool.isRequired,
+ checked: PropTypes.bool.isRequired,
+ disabled: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+export default function TelemetryBannerContainer(props) {
+ const {
+ bannerVisible,
+ closeBanner,
+ optedIn,
+ disabled,
+ toggleWebStoriesTrackingOptIn,
+ } = useTelemetryOptIn();
+
+ return (
+
+ );
+}
diff --git a/assets/src/dashboard/app/views/shared/test/telemetryBanner.js b/assets/src/dashboard/app/views/shared/test/telemetryBanner.js
new file mode 100644
index 000000000000..e4963d3b9bde
--- /dev/null
+++ b/assets/src/dashboard/app/views/shared/test/telemetryBanner.js
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * External dependencies
+ */
+import { useState } from 'react';
+import userEvent from '@testing-library/user-event';
+
+/**
+ * Internal dependencies
+ */
+import { TelemetryOptInBanner } from '../telemetryBanner';
+import { renderWithTheme } from '../../../../testUtils';
+
+function TelemetryBannerTestContainer(props) {
+ const [state, setState] = useState({
+ visible: true,
+ checked: false,
+ disabled: false,
+ ...props,
+ });
+
+ const onClose = () =>
+ setState((prevState) => ({ ...prevState, visible: false }));
+
+ const onChange = () => {
+ setState((prevState) => ({
+ ...prevState,
+ checked: true,
+ }));
+ };
+
+ return (
+
+ );
+}
+
+describe('TelemetryBanner', () => {
+ it('should render visible with the checkbox unchecked', () => {
+ const { getByRole } = renderWithTheme();
+
+ const checkbox = getByRole('checkbox');
+
+ expect(checkbox).not.toBeChecked();
+ });
+
+ it('should change the checkbox to checked and update the header when clicked', () => {
+ const { getByRole, getByText } = renderWithTheme(
+
+ );
+
+ const checkbox = getByRole('checkbox');
+
+ expect(checkbox).not.toBeChecked();
+
+ let bannerHeader = getByText(/Help improve the editor!/);
+
+ expect(bannerHeader).toBeInTheDocument();
+
+ userEvent.click(checkbox);
+
+ expect(checkbox).toBeChecked();
+
+ bannerHeader = getByText(/Your selection has been updated./);
+ expect(bannerHeader).toBeInTheDocument();
+ });
+
+ it('should close and not be visible when the close icon is clicked', () => {
+ const { getByRole } = renderWithTheme();
+
+ const closeButton = getByRole('button');
+
+ expect(closeButton).toBeInTheDocument();
+
+ userEvent.click(closeButton);
+
+ expect(closeButton).not.toBeInTheDocument();
+ });
+
+ it('should not be able to be checked when disabled', () => {
+ const { getByRole } = renderWithTheme(
+
+ );
+
+ const checkbox = getByRole('checkbox');
+
+ expect(checkbox).not.toBeChecked();
+
+ userEvent.click(checkbox);
+
+ expect(checkbox).not.toBeChecked();
+ });
+});
diff --git a/assets/src/dashboard/app/views/shared/useTelemetryOptIn.js b/assets/src/dashboard/app/views/shared/useTelemetryOptIn.js
new file mode 100644
index 000000000000..88e4f1acbe90
--- /dev/null
+++ b/assets/src/dashboard/app/views/shared/useTelemetryOptIn.js
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * External dependencies
+ */
+import { useCallback, useState, useEffect, useRef } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import useApi from '../../api/useApi';
+import { useRouteHistory } from '../../router';
+import { APP_ROUTES } from '../../../constants';
+
+// The value associated with this key indicates if the user has interacted with
+// the banner previously. If they have, we do not show the banner again.
+const LOCAL_STORAGE_KEY = 'web_stories_tracking_optin_banner_closed';
+
+function setInitialBannerPreviouslyClosed() {
+ const storageValue = localStorage.getItem(LOCAL_STORAGE_KEY);
+
+ return Boolean(JSON.parse(storageValue));
+}
+
+export default function useTelemetryOptIn() {
+ const [bannerPreviouslyClosed, setBannerPreviouslyClosed] = useState(
+ setInitialBannerPreviouslyClosed
+ );
+ const [optInCheckboxClicked, setOptInCheckboxClicked] = useState(false);
+ const {
+ currentUser,
+ toggleWebStoriesTrackingOptIn,
+ fetchCurrentUser,
+ } = useApi(
+ ({
+ state: { currentUser },
+ actions: {
+ usersApi: { toggleWebStoriesTrackingOptIn, fetchCurrentUser },
+ },
+ }) => ({ currentUser, toggleWebStoriesTrackingOptIn, fetchCurrentUser })
+ );
+ const { currentPath } = useRouteHistory(({ state: { currentPath } }) => ({
+ currentPath,
+ }));
+
+ const dataIsLoaded =
+ currentUser.data.meta?.web_stories_tracking_optin !== undefined;
+
+ const optedIn = Boolean(currentUser.data.meta?.web_stories_tracking_optin);
+
+ const dataFetched = useRef(false);
+
+ useEffect(() => {
+ if (!dataIsLoaded && !dataFetched.current) {
+ fetchCurrentUser();
+ dataFetched.current = true;
+ }
+ }, [dataIsLoaded, fetchCurrentUser]);
+
+ const _toggleWebStoriesTrackingOptIn = useCallback(() => {
+ toggleWebStoriesTrackingOptIn();
+ localStorage.setItem(LOCAL_STORAGE_KEY, true);
+ setOptInCheckboxClicked(true);
+ }, [toggleWebStoriesTrackingOptIn]);
+
+ const closeBanner = useCallback(() => {
+ setBannerPreviouslyClosed(true);
+ localStorage.setItem(LOCAL_STORAGE_KEY, true);
+ }, []);
+
+ let bannerVisible = true;
+
+ if (
+ bannerPreviouslyClosed || // The banner has been closed before
+ currentPath === APP_ROUTES.EDITOR_SETTINGS || // The user is on the settings page
+ !dataIsLoaded || // currentUser is not loaded yet
+ (!optInCheckboxClicked && optedIn) // currentUser is loaded and optedIn is true but the user has not checked the opt in checkbox
+ ) {
+ bannerVisible = false;
+ }
+
+ return {
+ bannerVisible,
+ optedIn,
+ disabled: currentUser.isUpdating,
+ closeBanner,
+ toggleWebStoriesTrackingOptIn: _toggleWebStoriesTrackingOptIn,
+ };
+}
diff --git a/assets/src/dashboard/components/pageStructure/index.js b/assets/src/dashboard/components/pageStructure/index.js
index 7f95a49e7397..ab8e76fbdaaf 100644
--- a/assets/src/dashboard/components/pageStructure/index.js
+++ b/assets/src/dashboard/components/pageStructure/index.js
@@ -100,17 +100,12 @@ export const LeftRailContainer = styled.nav.attrs({
export function LeftRail() {
const { state } = useRouteHistory();
- const {
- newStoryURL,
- version,
- capabilities: { canManageSettings } = {},
- } = useConfig();
+ const { newStoryURL, version } = useConfig();
const leftRailRef = useRef(null);
const upperContentRef = useRef(null);
const enableInProgressViews = useFeature('enableInProgressViews');
- const enableSettingsViews =
- useFeature('enableSettingsView') && canManageSettings;
+ const enableSettingsViews = useFeature('enableSettingsView');
const {
state: { sideBarVisible },
diff --git a/assets/src/dashboard/constants/index.js b/assets/src/dashboard/constants/index.js
index d8f886829fed..e20ba8a9d967 100644
--- a/assets/src/dashboard/constants/index.js
+++ b/assets/src/dashboard/constants/index.js
@@ -106,6 +106,7 @@ export const VIEW_STYLE_LABELS = {
export const ICON_METRICS = {
VIEW_STYLE: { width: 17, height: 14 },
LEFT_RIGHT_ARROW: { width: 16, height: 16 },
+ TELEMETRY_BANNER_EXIT: { width: 10, height: 10 },
};
export const DASHBOARD_VIEWS = {
diff --git a/assets/src/dashboard/karma/fixture.js b/assets/src/dashboard/karma/fixture.js
index a6f562732998..9b4fa089bfba 100644
--- a/assets/src/dashboard/karma/fixture.js
+++ b/assets/src/dashboard/karma/fixture.js
@@ -190,6 +190,7 @@ export default class Fixture {
restore() {
window.location.hash = '#';
+ localStorage.clear();
}
/**
diff --git a/assets/src/dashboard/testUtils/index.js b/assets/src/dashboard/testUtils/index.js
index 06ef3365e47e..3a27fd3ac18e 100644
--- a/assets/src/dashboard/testUtils/index.js
+++ b/assets/src/dashboard/testUtils/index.js
@@ -15,5 +15,6 @@
*/
export {
default as renderWithTheme,
+ renderWithProviders,
renderWithThemeAndFlagsProvider,
} from './renderWithTheme';
diff --git a/assets/src/dashboard/testUtils/mockApiProvider.js b/assets/src/dashboard/testUtils/mockApiProvider.js
new file mode 100644
index 000000000000..c632d9ea05e5
--- /dev/null
+++ b/assets/src/dashboard/testUtils/mockApiProvider.js
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * External dependencies
+ */
+import { useMemo, useState } from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * Internal dependencies
+ */
+
+import { ApiContext } from '../app/api/apiProvider';
+
+const noop = () => {};
+
+export default function MockApiProvider({ children }) {
+ const [currentUser, setCurrentUser] = useState(getCurrentUserState());
+
+ const usersApi = useMemo(
+ () => ({
+ fetchUsers: noop,
+ fetchCurrentUser: noop,
+ toggleWebStoriesTrackingOptIn: () =>
+ setCurrentUser(toggleOptInTracking(currentUser)),
+ }),
+ [currentUser]
+ );
+
+ const value = useMemo(
+ () => ({
+ state: { currentUser },
+ actions: { usersApi },
+ }),
+ [currentUser, usersApi]
+ );
+ return {children};
+}
+
+MockApiProvider.propTypes = {
+ children: PropTypes.node,
+};
+
+function getCurrentUserState() {
+ return {
+ data: { id: 1, meta: { web_stories_tracking_optin: false } },
+ isUpdating: false,
+ };
+}
+
+function toggleOptInTracking(currentUser) {
+ return {
+ ...currentUser,
+ data: {
+ ...currentUser.data,
+ meta: {
+ web_stories_tracking_optin: !currentUser.data.meta
+ .web_stories_tracking_optin,
+ },
+ },
+ };
+}
diff --git a/assets/src/dashboard/testUtils/renderWithTheme.js b/assets/src/dashboard/testUtils/renderWithTheme.js
index 5a9edfb26973..e3fe9569e78d 100644
--- a/assets/src/dashboard/testUtils/renderWithTheme.js
+++ b/assets/src/dashboard/testUtils/renderWithTheme.js
@@ -25,6 +25,8 @@ import { FlagsProvider } from 'flagged';
* Internal dependencies
*/
import theme from '../theme';
+import { ConfigProvider } from '../app/config';
+import MockApiProvider from './mockApiProvider';
// eslint-disable-next-line react/prop-types
const WithThemeProvider = ({ children }) => {
@@ -41,3 +43,30 @@ export const renderWithThemeAndFlagsProvider = (ui, featureFlags = {}) => {
{ui}
);
};
+
+const defaultProviderValues = {
+ features: {},
+ theme,
+ config: {},
+};
+
+// Please use renderWithProviders instead of renderWithTheme or renderWithThemeAndFlagsProvider
+// and feel free to add provider/mock provider as needed to this util.
+// TODO: deprecate and replace instances of the above render utils
+export const renderWithProviders = (
+ ui,
+ providerValues = {},
+ renderOptions = {}
+) => {
+ const mergedProviderValues = { ...defaultProviderValues, ...providerValues };
+ return render(
+
+
+
+ {ui}
+
+
+ ,
+ renderOptions
+ );
+};
diff --git a/includes/Tracking.php b/includes/Tracking.php
index 7eabb998e8e7..1657f2117aff 100644
--- a/includes/Tracking.php
+++ b/includes/Tracking.php
@@ -70,7 +70,7 @@ public function init() {
'default' => false,
'show_in_rest' => true,
'auth_callback' => static function() {
- return current_user_can( 'manage_options' );
+ return current_user_can( 'edit_user', get_current_user_id() );
},
'single' => true,
]
diff --git a/package-lock.json b/package-lock.json
index 4c0e6bd269ff..ac154423dad3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8852,6 +8852,26 @@
"@types/testing-library__react-hooks": "^3.3.0"
}
},
+ "@testing-library/user-event": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.1.3.tgz",
+ "integrity": "sha512-U6tpKWbBMvqt8tIF77crr9VyP1W+yxK+c48xH5rvYwmT4MER5jvWAFqNzkn542Bt3qeDCn0aqwb0Pv+3mDbLtw==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.10.2"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.11.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
+ "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
+ "dev": true,
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
+ }
+ },
"@types/anymatch": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz",
diff --git a/package.json b/package.json
index 7c9a906678d7..1e2017774316 100644
--- a/package.json
+++ b/package.json
@@ -93,6 +93,7 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.0.2",
"@testing-library/react-hooks": "^3.4.1",
+ "@testing-library/user-event": "^12.1.3",
"@wordpress/babel-plugin-import-jsx-pragma": "^2.7.0",
"@wordpress/dependency-extraction-webpack-plugin": "^2.8.0",
"@wordpress/e2e-test-utils": "^4.11.2",
diff --git a/tests/phpunit/tests/Media.php b/tests/phpunit/tests/Media.php
index 0070a704f1e6..859f79ae7d83 100644
--- a/tests/phpunit/tests/Media.php
+++ b/tests/phpunit/tests/Media.php
@@ -61,7 +61,7 @@ public function test_rest_api_init() {
set_post_thumbnail( $video_attachment_id, $poster_attachment_id );
wp_set_object_terms( $video_attachment_id, 'editor', \Google\Web_Stories\Media::STORY_MEDIA_TAXONOMY );
- $request = new WP_REST_Request( 'GET', sprintf( '/web-stories/v1/media/%d', $video_attachment_id ) );
+ $request = new WP_REST_Request( \WP_REST_Server::READABLE, sprintf( '/web-stories/v1/media/%d', $video_attachment_id ) );
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
diff --git a/tests/phpunit/tests/REST_API/Stories_Autosaves_Controller.php b/tests/phpunit/tests/REST_API/Stories_Autosaves_Controller.php
index 6d16a4c17a64..7fdcf631cc1f 100644
--- a/tests/phpunit/tests/REST_API/Stories_Autosaves_Controller.php
+++ b/tests/phpunit/tests/REST_API/Stories_Autosaves_Controller.php
@@ -67,7 +67,7 @@ public function test_create_item_as_author_should_not_strip_markup() {
]
);
- $request = new WP_REST_Request( 'POST', '/web-stories/v1/web-story/' . $story . '/autosaves' );
+ $request = new WP_REST_Request( \WP_REST_Server::CREATABLE, '/web-stories/v1/web-story/' . $story . '/autosaves' );
$request->set_body_params(
[
'content' => $unsanitized_content,
diff --git a/tests/phpunit/tests/REST_API/Stories_Controller.php b/tests/phpunit/tests/REST_API/Stories_Controller.php
index e353f1e308bd..d97fcc04c7a7 100644
--- a/tests/phpunit/tests/REST_API/Stories_Controller.php
+++ b/tests/phpunit/tests/REST_API/Stories_Controller.php
@@ -152,7 +152,7 @@ public function test_register_routes() {
*/
public function test_get_items() {
wp_set_current_user( self::$user_id );
- $request = new WP_REST_Request( 'GET', '/web-stories/v1/web-story' );
+ $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/web-story' );
$request->set_param( 'status', [ 'draft' ] );
$request->set_param( 'context', 'edit' );
$response = rest_get_server()->dispatch( $request );
@@ -177,7 +177,7 @@ public function test_get_items() {
*/
public function test_get_items_format() {
wp_set_current_user( self::$user_id );
- $request = new WP_REST_Request( 'GET', '/web-stories/v1/web-story' );
+ $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/web-story' );
$request->set_param( 'status', [ 'draft' ] );
$request->set_param( 'context', 'edit' );
$request->set_param( '_web_stories_envelope', true );
@@ -219,7 +219,7 @@ public function test_get_item_schema() {
* @covers ::filter_posts_clauses
*/
public function test_filter_posts_by_author_display_names() {
- $request = new WP_REST_Request( 'GET', '/web-stories/v1/web-story' );
+ $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/web-story' );
$request->set_param( 'order', 'asc' );
$request->set_param( 'orderby', 'story_author' );
@@ -240,7 +240,7 @@ public function test_filter_posts_by_author_display_names() {
'Expected posts ordered by author display names'
);
- $request = new WP_REST_Request( 'GET', '/web-stories/v1/web-story' );
+ $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/web-story' );
$request->set_param( 'order', 'desc' );
$request->set_param( 'orderby', 'story_author' );
@@ -298,7 +298,7 @@ public function test_create_item_as_author_should_not_strip_markup() {
$unsanitized_content = file_get_contents( __DIR__ . '/../../data/story_post_content.html' );
$unsanitized_story_data = json_decode( file_get_contents( __DIR__ . '/../../data/story_post_content_filtered.json' ), true );
- $request = new WP_REST_Request( 'POST', '/web-stories/v1/web-story' );
+ $request = new WP_REST_Request( \WP_REST_Server::CREATABLE, '/web-stories/v1/web-story' );
$request->set_body_params(
[
'content' => $unsanitized_content,
@@ -328,7 +328,7 @@ public function test_update_item_as_author_should_not_strip_markup() {
]
);
- $request = new WP_REST_Request( 'PUT', '/web-stories/v1/web-story/' . $story );
+ $request = new WP_REST_Request( \WP_REST_Server::CREATABLE, '/web-stories/v1/web-story/' . $story );
$request->set_body_params(
[
'content' => $unsanitized_content,
@@ -362,7 +362,7 @@ public function test_update_item_publisher_id() {
$attachment_id = self::factory()->attachment->create_upload_object( __DIR__ . '/../../data/attachment.jpg', 0 );
- $request = new WP_REST_Request( 'PUT', '/web-stories/v1/web-story/' . $story );
+ $request = new WP_REST_Request( \WP_REST_Server::CREATABLE, '/web-stories/v1/web-story/' . $story );
$request->set_body_params(
[
'content' => $unsanitized_content,
diff --git a/tests/phpunit/tests/REST_API/Stories_Media_Controller.php b/tests/phpunit/tests/REST_API/Stories_Media_Controller.php
index 2cdb53737da0..d77609907dcf 100644
--- a/tests/phpunit/tests/REST_API/Stories_Media_Controller.php
+++ b/tests/phpunit/tests/REST_API/Stories_Media_Controller.php
@@ -76,7 +76,7 @@ public function tearDown() {
*/
public function test_get_items_format() {
wp_set_current_user( self::$user_id );
- $request = new WP_REST_Request( 'GET', '/web-stories/v1/media' );
+ $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/media' );
$request->set_param( 'context', 'edit' );
$request->set_param( '_web_stories_envelope', true );
$response = rest_get_server()->dispatch( $request );
diff --git a/tests/phpunit/tests/Tracking.php b/tests/phpunit/tests/Tracking.php
index 8db364abd718..7d134ca76991 100644
--- a/tests/phpunit/tests/Tracking.php
+++ b/tests/phpunit/tests/Tracking.php
@@ -26,16 +26,30 @@
class Tracking extends \WP_UnitTestCase {
protected static $user_id;
+ protected static $author_id;
+
public static function wpSetUpBeforeClass( $factory ) {
self::$user_id = $factory->user->create(
[
'role' => 'administrator',
]
);
+
+ self::$author_id = $factory->user->create(
+ [
+ 'role' => 'author',
+ ]
+ );
}
public static function wpTearDownAfterClass() {
self::delete_user( self::$user_id );
+ self::delete_user( self::$author_id );
+ }
+
+ public function tearDown() {
+ unregister_meta_key( 'user', \Google\Web_Stories\Tracking::OPTIN_META_KEY );
+ parent::tearDown();
}
/**
@@ -46,13 +60,57 @@ public function test_add_optin_field_to_rest_api() {
( new \Google\Web_Stories\Tracking() )->init();
add_user_meta( get_current_user_id(), \Google\Web_Stories\Tracking::OPTIN_META_KEY, true );
- $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', self::$user_id ) );
+ $request = new WP_REST_Request( \WP_REST_Server::READABLE, sprintf( '/wp/v2/users/%d', self::$user_id ) );
$response = rest_get_server()->dispatch( $request );
$data = $response->get_data();
+
+ $this->assertArrayHasKey( 'meta', $data );
$this->assertArrayHasKey( \Google\Web_Stories\Tracking::OPTIN_META_KEY, $data['meta'] );
$this->assertTrue( $data['meta'][ \Google\Web_Stories\Tracking::OPTIN_META_KEY ] );
}
+ /**
+ * @covers ::init
+ */
+ public function test_add_optin_field_to_rest_api_for_author_user() {
+ wp_set_current_user( self::$author_id );
+ ( new \Google\Web_Stories\Tracking() )->init();
+ add_user_meta( get_current_user_id(), \Google\Web_Stories\Tracking::OPTIN_META_KEY, true );
+
+ $request = new WP_REST_Request( \WP_REST_Server::READABLE, '/wp/v2/users/me' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertArrayHasKey( 'meta', $data );
+ $this->assertArrayHasKey( \Google\Web_Stories\Tracking::OPTIN_META_KEY, $data['meta'] );
+ $this->assertTrue( $data['meta'][ \Google\Web_Stories\Tracking::OPTIN_META_KEY ] );
+ }
+
+ /**
+ * @covers ::init
+ */
+ public function test_enables_author_user_to_update_meta_field() {
+ wp_set_current_user( self::$author_id );
+ ( new \Google\Web_Stories\Tracking() )->init();
+ add_user_meta( get_current_user_id(), \Google\Web_Stories\Tracking::OPTIN_META_KEY, false );
+
+ $request = new WP_REST_Request( \WP_REST_Server::CREATABLE, '/wp/v2/users/me' );
+ $request->set_body_params(
+ [
+ 'meta' => [
+ \Google\Web_Stories\Tracking::OPTIN_META_KEY => true,
+ ],
+ ]
+ );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+
+ $this->assertArrayHasKey( 'meta', $data );
+ $this->assertArrayHasKey( \Google\Web_Stories\Tracking::OPTIN_META_KEY, $data['meta'] );
+ $this->assertTrue( $data['meta'][ \Google\Web_Stories\Tracking::OPTIN_META_KEY ] );
+ }
+
+
/**
* @covers ::init
*/