mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 07:03:38 +00:00
Feature/nps (#17570)
Co-authored-by: markkaylor <mark.kaylor@strapi.io> Co-authored-by: Gustav Hansen <gustav.hansen@strapi.io> Co-authored-by: Rémi de Juvigny <8087692+remidej@users.noreply.github.com> Co-authored-by: Fernando Chavez <fernando.chavez@strapi.io> Co-authored-by: Madhuri Sandbhor <madhurisandbhor@gmail.com>
This commit is contained in:
parent
d25fed47e8
commit
5785e8f4f0
30
docs/docs/docs/01-core/admin/05-nps.md
Normal file
30
docs/docs/docs/01-core/admin/05-nps.md
Normal file
@ -0,0 +1,30 @@
|
||||
---
|
||||
title: NPS
|
||||
tags:
|
||||
- admin
|
||||
- nps
|
||||
---
|
||||
|
||||
## What does it do
|
||||
|
||||
The NPS survey is shown to users to get their feedback about Strapi. It is based on a rating scale from 0 to 10, and we also invite users to provide additional comments.
|
||||
|
||||
## When do we show the survey?
|
||||
|
||||
The NPS survey is only displayed to admin users who have selected the "Keep me updated" checkbox during registration. The survey is displayed after 5 minutes of activity.
|
||||
|
||||
The survey is shown to eligible users based on the following rules:
|
||||
|
||||
- If a user responds to the survey, the survey will be presented again within 90 days.
|
||||
- If a user does not respond to the survey the first time after their last response, the survey will be presented again after 7 days.
|
||||
- If a user does not respond to the survey for the second or subsequent time after their last response, the survey will be presented again after 90 days.
|
||||
|
||||
## Where data is submitted
|
||||
|
||||
The data is sent to this endpoint: `https://analytics.strapi.io/submit-nps`.
|
||||
|
||||
## Hooks
|
||||
|
||||
### useNpsSurveySettings
|
||||
|
||||
This hook uses the `usePersistentState` hook from the helper-plugin (more information available [here](/docs/core/helper-plugin/hooks/use-persistent-state)). It is exported so that it can be used during the registration process to determine whether users have selected the "Keep me updated" checkbox.
|
||||
@ -0,0 +1,27 @@
|
||||
---
|
||||
title: usePersistentState
|
||||
description: API reference for the usePersistentState hook in Strapi
|
||||
tags:
|
||||
- hooks
|
||||
- helper-plugin
|
||||
---
|
||||
|
||||
Provides a easily usable hook to store data on the local storage.
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { usePersistentState } from '@strapi/helper-plugin';
|
||||
|
||||
const MyComponent = () => {
|
||||
const [navbarOpened, setNavbarOpened] = usePersistentState('navbar-open', true);
|
||||
|
||||
return <nav>{navbarOpened ? <div>My menu!</div> : null}</nav>;
|
||||
};
|
||||
```
|
||||
|
||||
## Typescript
|
||||
|
||||
```ts
|
||||
function usePersistentState<T>(key: string, defaultValue: T): [T, Dispatch<SetStateAction<T>>];
|
||||
```
|
||||
@ -18,6 +18,7 @@ import { Admin } from '../pages/Admin';
|
||||
import { getFullName } from '../utils/getFullName';
|
||||
import { hashAdminUserEmail } from '../utils/uniqueAdminHash';
|
||||
|
||||
import NpsSurvey from './NpsSurvey';
|
||||
import RBACProvider from './RBACProvider';
|
||||
|
||||
const strapiVersion = packageJSON.version;
|
||||
@ -222,6 +223,7 @@ export const AuthenticatedApp = () => {
|
||||
userDisplayName={userDisplayName}
|
||||
>
|
||||
<RBACProvider permissions={permissions} refetchPermissions={refetch}>
|
||||
<NpsSurvey />
|
||||
<Admin />
|
||||
</RBACProvider>
|
||||
</AppInfoProvider>
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import { usePersistentState } from '@strapi/helper-plugin';
|
||||
|
||||
// Exported to make it available during admin user registration.
|
||||
// Because we only enable the NPS for users who subscribe to the newsletter when signing up
|
||||
export function useNpsSurveySettings() {
|
||||
const [npsSurveySettings, setNpsSurveySettings] = usePersistentState(
|
||||
'STRAPI_NPS_SURVEY_SETTINGS',
|
||||
{
|
||||
enabled: true,
|
||||
lastResponseDate: null,
|
||||
firstDismissalDate: null,
|
||||
lastDismissalDate: null,
|
||||
}
|
||||
);
|
||||
|
||||
return { npsSurveySettings, setNpsSurveySettings };
|
||||
}
|
||||
365
packages/core/admin/admin/src/components/NpsSurvey/index.js
Normal file
365
packages/core/admin/admin/src/components/NpsSurvey/index.js
Normal file
@ -0,0 +1,365 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
IconButton,
|
||||
Button,
|
||||
Typography,
|
||||
Textarea,
|
||||
Portal,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldInput,
|
||||
VisuallyHidden,
|
||||
} from '@strapi/design-system';
|
||||
import { auth, useNotification, useAppInfo } from '@strapi/helper-plugin';
|
||||
import { Cross } from '@strapi/icons';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useMutation } from 'react-query';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { useNpsSurveySettings } from './hooks/useNpsSurveySettings';
|
||||
|
||||
const FieldWrapper = styled(Field)`
|
||||
height: ${32 / 16}rem;
|
||||
width: ${32 / 16}rem;
|
||||
|
||||
> label,
|
||||
~ input {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
> label {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: ${({ theme }) => theme.spaces[2]};
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: ${({ theme }) => theme.colors.neutral0};
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.selected {
|
||||
color: ${({ theme }) => theme.colors.primary700};
|
||||
background-color: ${({ theme }) => theme.colors.neutral0};
|
||||
border-color: ${({ theme }) => theme.colors.primary700};
|
||||
}
|
||||
`;
|
||||
|
||||
const delays = {
|
||||
postResponse: 90 * 24 * 60 * 60 * 1000, // 90 days in ms
|
||||
postFirstDismissal: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
|
||||
postSubsequentDismissal: 90 * 24 * 60 * 60 * 1000, // 90 days in ms
|
||||
display: 5 * 60 * 1000, // 5 minutes in ms
|
||||
};
|
||||
|
||||
const ratingArray = [...Array(11).keys()];
|
||||
|
||||
const checkIfShouldShowSurvey = (settings) => {
|
||||
const { enabled, lastResponseDate, firstDismissalDate, lastDismissalDate } = settings;
|
||||
|
||||
// This function goes through all the cases where we'd want to not show the survey:
|
||||
// 1. If the survey is disabled, abort mission, don't bother checking the other settings.
|
||||
// 2. If the user has already responded to the survey, check if enough time has passed since the last response.
|
||||
// 3. If the user has dismissed the survey twice or more before, check if enough time has passed since the last dismissal.
|
||||
// 4. If the user has only dismissed the survey once before, check if enough time has passed since the first dismissal.
|
||||
// If none of these cases check out, then we show the survey.
|
||||
// Note that submitting a response resets the dismissal counts.
|
||||
// Checks 3 and 4 should not be reversed, since the first dismissal will also exist if the user has dismissed the survey twice or more before.
|
||||
|
||||
// User hasn't enabled NPS feature
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user has already responded to the survey
|
||||
if (lastResponseDate) {
|
||||
const timeSinceLastResponse = Date.now() - new Date(lastResponseDate).getTime();
|
||||
|
||||
if (timeSinceLastResponse >= delays.postResponse) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user has dismissed the survey twice or more before
|
||||
if (lastDismissalDate) {
|
||||
const timeSinceLastDismissal = Date.now() - new Date(lastDismissalDate).getTime();
|
||||
|
||||
if (timeSinceLastDismissal >= delays.postSubsequentDismissal) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user has only dismissed the survey once before
|
||||
if (firstDismissalDate) {
|
||||
const timeSinceFirstDismissal = Date.now() - new Date(firstDismissalDate).getTime();
|
||||
|
||||
if (timeSinceFirstDismissal >= delays.postFirstDismissal) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user has not interacted with the survey before
|
||||
return true;
|
||||
};
|
||||
|
||||
const NpsSurvey = () => {
|
||||
const theme = useTheme();
|
||||
const { formatMessage } = useIntl();
|
||||
const { npsSurveySettings, setNpsSurveySettings } = useNpsSurveySettings();
|
||||
const [isFeedbackResponse, setIsFeedbackResponse] = React.useState(false);
|
||||
const toggleNotification = useNotification();
|
||||
const { currentEnvironment, strapiVersion } = useAppInfo();
|
||||
|
||||
const { mutate, isLoading } = useMutation(
|
||||
async (form) => {
|
||||
const res = await fetch('https://analytics.strapi.io/submit-nps', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(form),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to submit NPS survey');
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
setNpsSurveySettings((settings) => ({
|
||||
...settings,
|
||||
lastResponseDate: new Date(),
|
||||
firstDismissalDate: null,
|
||||
lastDismissalDate: null,
|
||||
}));
|
||||
setIsFeedbackResponse(true);
|
||||
// Thank you message displayed in the banner should disappear after few seconds.
|
||||
setTimeout(() => {
|
||||
setSurveyIsShown(false);
|
||||
}, 3000);
|
||||
},
|
||||
onError() {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatMessage({ id: 'notification.error' }),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Only check on first render if the survey should be shown
|
||||
const [surveyIsShown, setSurveyIsShown] = React.useState(
|
||||
checkIfShouldShowSurvey(npsSurveySettings)
|
||||
);
|
||||
|
||||
// Set a cooldown to show the survey when session begins
|
||||
const [displaySurvey, setDisplaySurvey] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const displayTime = setTimeout(() => {
|
||||
setDisplaySurvey(true);
|
||||
}, delays.display);
|
||||
|
||||
return () => {
|
||||
clearTimeout(displayTime);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!displaySurvey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!surveyIsShown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSubmitResponse = ({ npsSurveyRating: rating, npsSurveyFeedback: comment }) => {
|
||||
const { email } = auth.getUserInfo();
|
||||
mutate({
|
||||
email,
|
||||
rating,
|
||||
comment,
|
||||
environment: currentEnvironment,
|
||||
version: strapiVersion,
|
||||
license: window.strapi.projectType,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
setNpsSurveySettings((settings) => {
|
||||
const nextSettings = {
|
||||
...settings,
|
||||
lastResponseDate: null,
|
||||
};
|
||||
|
||||
if (settings.firstDismissalDate) {
|
||||
// If the user dismisses the survey for the second time
|
||||
nextSettings.lastDismissalDate = new Date();
|
||||
} else {
|
||||
// If the user dismisses the survey for the first time
|
||||
nextSettings.firstDismissalDate = new Date();
|
||||
}
|
||||
|
||||
return nextSettings;
|
||||
});
|
||||
|
||||
setSurveyIsShown(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Formik
|
||||
initialValues={{ npsSurveyFeedback: '', npsSurveyRating: null }}
|
||||
onSubmit={handleSubmitResponse}
|
||||
validationSchema={yup.object({
|
||||
npsSurveyFeedback: yup.string(),
|
||||
npsSurveyRating: yup.number().required(),
|
||||
})}
|
||||
>
|
||||
{({ values, handleChange, setFieldValue }) => (
|
||||
<Form name="npsSurveyForm">
|
||||
<Flex
|
||||
hasRadius
|
||||
direction="column"
|
||||
padding={4}
|
||||
borderColor="primary200"
|
||||
background="neutral0"
|
||||
shadow="popupShadow"
|
||||
position="fixed"
|
||||
bottom={0}
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
zIndex={theme.zIndices[2]}
|
||||
width="50%"
|
||||
>
|
||||
{isFeedbackResponse ? (
|
||||
<Typography fontWeight="semiBold">
|
||||
{formatMessage({
|
||||
id: 'app.components.NpsSurvey.feedback-response',
|
||||
defaultMessage: 'Thank you very much for your feedback!',
|
||||
})}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box as="fieldset" width="100%">
|
||||
<Flex justifyContent="space-between" width="100%">
|
||||
<Box marginLeft="auto" marginRight="auto">
|
||||
<Typography fontWeight="semiBold" as="legend">
|
||||
{formatMessage({
|
||||
id: 'app.components.NpsSurvey.banner-title',
|
||||
defaultMessage:
|
||||
'How likely are you to recommend Strapi to a friend or colleague?',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton
|
||||
onClick={handleDismiss}
|
||||
aria-label={formatMessage({
|
||||
id: 'app.components.NpsSurvey.dismiss-survey-label',
|
||||
defaultMessage: 'Dismiss survey',
|
||||
})}
|
||||
icon={<Cross />}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap={2} marginTop={2} marginBottom={2} justifyContent="center">
|
||||
<Typography variant="pi" textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: 'app.components.NpsSurvey.no-recommendation',
|
||||
defaultMessage: 'Not at all likely',
|
||||
})}
|
||||
</Typography>
|
||||
{ratingArray.map((number) => {
|
||||
return (
|
||||
<FieldWrapper
|
||||
key={number}
|
||||
className={values.npsSurveyRating === number ? 'selected' : null} // "selected" class added when child radio button is checked
|
||||
hasRadius
|
||||
background="primary100"
|
||||
borderColor="primary200"
|
||||
color="primary600"
|
||||
position="relative"
|
||||
cursor="pointer"
|
||||
>
|
||||
<FieldLabel htmlFor={`nps-survey-rating-${number}-input`}>
|
||||
<VisuallyHidden>
|
||||
<FieldInput
|
||||
type="radio"
|
||||
id={`nps-survey-rating-${number}-input`}
|
||||
name="npsSurveyRating"
|
||||
checked={values.npsSurveyRating === number}
|
||||
onChange={(e) =>
|
||||
setFieldValue('npsSurveyRating', parseInt(e.target.value, 10))
|
||||
}
|
||||
value={number}
|
||||
/>
|
||||
</VisuallyHidden>
|
||||
{number}
|
||||
</FieldLabel>
|
||||
</FieldWrapper>
|
||||
);
|
||||
})}
|
||||
<Typography variant="pi" textColor="neutral600">
|
||||
{formatMessage({
|
||||
id: 'app.components.NpsSurvey.happy-to-recommend',
|
||||
defaultMessage: 'Extremely likely',
|
||||
})}
|
||||
</Typography>
|
||||
</Flex>
|
||||
{values.npsSurveyRating !== null && (
|
||||
<Flex direction="column">
|
||||
<Box marginTop={2}>
|
||||
<FieldLabel htmlFor="npsSurveyFeedback" fontWeight="semiBold" fontSize={2}>
|
||||
{formatMessage({
|
||||
id: 'app.components.NpsSurvey.feedback-question',
|
||||
defaultMessage: 'Do you have any suggestion for improvements?',
|
||||
})}
|
||||
</FieldLabel>
|
||||
</Box>
|
||||
<Box width="62%" marginTop={3} marginBottom={4}>
|
||||
<Textarea
|
||||
id="npsSurveyFeedback" // formik element attribute "id" should be same as the values key to work
|
||||
width="100%"
|
||||
onChange={handleChange}
|
||||
>
|
||||
{values.npsSurveyFeedback}
|
||||
</Textarea>
|
||||
</Box>
|
||||
<Button marginBottom={2} type="submit" loading={isLoading}>
|
||||
{formatMessage({
|
||||
id: 'app.components.NpsSurvey.submit-feedback',
|
||||
defaultMessage: 'Submit Feedback',
|
||||
})}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NpsSurvey;
|
||||
@ -0,0 +1,290 @@
|
||||
import React from 'react';
|
||||
|
||||
import { lightTheme, ThemeProvider } from '@strapi/design-system';
|
||||
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import NpsSurvey from '..';
|
||||
|
||||
const toggleNotification = jest.fn();
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
...jest.requireActual('@strapi/helper-plugin'),
|
||||
auth: {
|
||||
getUserInfo: jest.fn(() => ({
|
||||
email: 'john@doe.com',
|
||||
})),
|
||||
},
|
||||
useNotification: jest.fn().mockImplementation(() => toggleNotification),
|
||||
useAppInfo: jest
|
||||
.fn()
|
||||
.mockImplementation(() => ({
|
||||
autoReload: true,
|
||||
strapiVersion: 'test',
|
||||
communityEdition: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
const handlers = [
|
||||
rest.post('*/submit-nps', (req, res, ctx) => {
|
||||
return res.once(ctx.status(200));
|
||||
}),
|
||||
];
|
||||
|
||||
const server = setupServer(...handlers);
|
||||
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
};
|
||||
const originalLocalStorage = global.localStorage;
|
||||
|
||||
const user = userEvent.setup({ delay: null });
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const setup = () =>
|
||||
render(<NpsSurvey />, {
|
||||
wrapper({ children }) {
|
||||
return (
|
||||
<IntlProvider locale="en" defaultLocale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={lightTheme}>{children}</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
describe('NPS survey', () => {
|
||||
beforeAll(() => {
|
||||
global.localStorage = localStorageMock;
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('renders survey if enabled', () => {
|
||||
localStorageMock.getItem.mockReturnValueOnce({ enabled: true });
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.getByLabelText('0')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('10')).toBeInTheDocument();
|
||||
expect(screen.getByText(/not at all likely/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/extremely likely/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render survey if disabled', () => {
|
||||
localStorageMock.getItem.mockReturnValueOnce({ enabled: false });
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('saves user response', async () => {
|
||||
localStorageMock.getItem.mockReturnValueOnce({ enabled: true });
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: '10' }));
|
||||
expect(screen.getByRole('button', { name: /submit feedback/i }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.submit(screen.getByRole('form'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/thank you very much for your feedback!/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const storedData = JSON.parse(localStorageMock.setItem.mock.calls.at(-1).at(1));
|
||||
expect(storedData).toEqual({
|
||||
enabled: true,
|
||||
lastResponseDate: expect.any(String),
|
||||
firstDismissalDate: null,
|
||||
lastDismissalDate: null,
|
||||
});
|
||||
expect(new Date(storedData.lastResponseDate)).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('show error message if request fails and keep survey open', async () => {
|
||||
server.use(
|
||||
rest.post('*/submit-nps', (req, res, ctx) => {
|
||||
return res.once(ctx.status(500));
|
||||
})
|
||||
);
|
||||
|
||||
localStorageMock.getItem.mockReturnValueOnce({ enabled: true });
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
|
||||
fireEvent.click(screen.getByRole('radio', { name: '10' }));
|
||||
expect(screen.getByRole('button', { name: /submit feedback/i }));
|
||||
|
||||
act(() => {
|
||||
fireEvent.submit(screen.getByRole('form'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/not at all likely/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/thank you very much for your feedback!/i)).not.toBeInTheDocument();
|
||||
expect(toggleNotification).toHaveBeenCalledWith({
|
||||
type: 'warning',
|
||||
message: 'notification.error',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('saves first user dismissal', async () => {
|
||||
localStorageMock.getItem.mockReturnValueOnce({ enabled: true });
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
|
||||
await user.click(screen.getByText(/dismiss survey/i));
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
|
||||
const storedData = JSON.parse(localStorageMock.setItem.mock.calls.at(-1).at(1));
|
||||
expect(storedData).toEqual({
|
||||
enabled: true,
|
||||
lastResponseDate: null,
|
||||
firstDismissalDate: expect.any(String),
|
||||
});
|
||||
expect(new Date(storedData.firstDismissalDate)).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('saves subsequent user dismissal', async () => {
|
||||
const firstDismissalDate = '2000-07-20T09:28:51.963Z';
|
||||
localStorageMock.getItem.mockReturnValueOnce({
|
||||
enabled: true,
|
||||
firstDismissalDate,
|
||||
});
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
await user.click(screen.getByText(/dismiss survey/i));
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
|
||||
const storedData = JSON.parse(localStorageMock.setItem.mock.calls.at(-1).at(1));
|
||||
expect(storedData).toEqual({
|
||||
enabled: true,
|
||||
lastResponseDate: null,
|
||||
firstDismissalDate,
|
||||
lastDismissalDate: expect.any(String),
|
||||
});
|
||||
expect(new Date(storedData.lastDismissalDate)).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('respects the delay after user submission', async () => {
|
||||
const initialDate = new Date('2020-01-01');
|
||||
const withinDelay = new Date('2020-01-31');
|
||||
const beyondDelay = new Date('2020-03-31');
|
||||
|
||||
localStorageMock.getItem.mockReturnValue({ enabled: true, lastResponseDate: initialDate });
|
||||
|
||||
jest.setSystemTime(initialDate);
|
||||
|
||||
// Survey should not show up right after submission
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
|
||||
// Survey should not show up during delay
|
||||
jest.advanceTimersByTime(withinDelay - initialDate);
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
|
||||
// Survey should show up again after delay
|
||||
jest.advanceTimersByTime(beyondDelay - withinDelay);
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.getByText(/not at all likely/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('respects the delay after first user dismissal', async () => {
|
||||
const initialDate = new Date('2020-01-01');
|
||||
const withinDelay = new Date('2020-01-04');
|
||||
const beyondDelay = new Date('2020-01-08');
|
||||
|
||||
localStorageMock.getItem.mockReturnValue({
|
||||
enabled: true,
|
||||
firstDismissalDate: initialDate,
|
||||
lastDismissalDate: null,
|
||||
lastResponseDate: null,
|
||||
});
|
||||
|
||||
jest.setSystemTime(initialDate);
|
||||
|
||||
// Survey should not show up right after dismissal
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
|
||||
// Survey should not show up during delay
|
||||
jest.advanceTimersByTime(withinDelay - initialDate);
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
|
||||
// Survey should show up again after delay
|
||||
jest.advanceTimersByTime(beyondDelay - withinDelay);
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.getByText(/not at all likely/i)).toBeInTheDocument();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('respects the delay after subsequent user dismissal', async () => {
|
||||
const initialDate = new Date('2020-01-01');
|
||||
const withinDelay = new Date('2020-03-30');
|
||||
const beyondDelay = new Date('2020-04-01');
|
||||
|
||||
localStorageMock.getItem.mockReturnValue({
|
||||
enabled: true,
|
||||
firstDismissalDate: initialDate,
|
||||
lastDismissalDate: initialDate,
|
||||
lastResponseDate: null,
|
||||
});
|
||||
|
||||
jest.setSystemTime(initialDate);
|
||||
|
||||
// Survey should not show up right after dismissal
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
|
||||
// Survey should not show up during delay
|
||||
jest.advanceTimersByTime(withinDelay - initialDate);
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.queryByText(/not at all likely/i)).not.toBeInTheDocument();
|
||||
|
||||
// Survey should show up again after delay
|
||||
jest.advanceTimersByTime(beyondDelay - withinDelay);
|
||||
setup();
|
||||
act(() => jest.runAllTimers());
|
||||
expect(screen.getByText(/not at all likely/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
global.localStorage = originalLocalStorage;
|
||||
});
|
||||
});
|
||||
@ -29,6 +29,7 @@ import { useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useNpsSurveySettings } from '../../../../components/NpsSurvey/hooks/useNpsSurveySettings';
|
||||
import Logo from '../../../../components/UnauthenticatedLogo';
|
||||
import UnauthenticatedLayout, { LayoutContent } from '../../../../layouts/UnauthenticatedLayout';
|
||||
import FieldActionWrapper from '../FieldActionWrapper';
|
||||
@ -55,6 +56,7 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
const query = useQuery();
|
||||
const { formatAPIError } = useAPIErrorHandler();
|
||||
const { get } = useFetchClient();
|
||||
const { setNpsSurveySettings } = useNpsSurveySettings();
|
||||
|
||||
const registrationToken = query.get('registrationToken');
|
||||
|
||||
@ -143,6 +145,9 @@ const Register = ({ authType, fieldsToDisable, noSignin, onSubmit, schema }) =>
|
||||
} else {
|
||||
onSubmit(normalizedData, formik);
|
||||
}
|
||||
|
||||
// Only enable EE survey if user accepted the newsletter
|
||||
setNpsSurveySettings({ enabled: data.news });
|
||||
} catch (err) {
|
||||
const errors = getYupInnerErrors(err);
|
||||
setSubmitCount(submitCount + 1);
|
||||
|
||||
@ -479,6 +479,13 @@
|
||||
"app.components.MarketplaceBanner.link": "Check it out now",
|
||||
"app.components.NotFoundPage.back": "Back to homepage",
|
||||
"app.components.NotFoundPage.description": "Not Found",
|
||||
"app.components.NpsSurvey.banner-title": "How likely are you to recommend Strapi to a friend or colleague?",
|
||||
"app.components.NpsSurvey.feedback-response": "Thank you very much for your feedback!",
|
||||
"app.components.NpsSurvey.feedback-question": "Do you have any suggestion for improvements?",
|
||||
"app.components.NpsSurvey.submit-feedback": "Submit Feedback",
|
||||
"app.components.NpsSurvey.dismiss-survey-label": "Dismiss survey",
|
||||
"app.components.NpsSurvey.no-recommendation": "Not at all likely",
|
||||
"app.components.NpsSurvey.happy-to-recommend": "Extremely likely",
|
||||
"app.components.Official": "Official",
|
||||
"app.components.Onboarding.help.button": "Help button",
|
||||
"app.components.Onboarding.label.completed": "% completed",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user