mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-29 20:11:32 +02:00
Merge pull request #28226 from overleaf/ls-remove-leaver-survey-alert
Remove leaver survey alert GitOrigin-RevId: 6dbeabaff8c73d2ce9e3e382da83ce8f2177668d
This commit is contained in:
@@ -858,7 +858,6 @@
|
||||
"institution_and_role": "",
|
||||
"institution_has_overleaf_subscription": "",
|
||||
"institution_templates": "",
|
||||
"institutional_leavers_survey_notification": "",
|
||||
"integrations": "",
|
||||
"integrations_like_github": "",
|
||||
"interested_in_cheaper_personal_plan": "",
|
||||
@@ -948,7 +947,6 @@
|
||||
"library": "",
|
||||
"licenses": "",
|
||||
"limited_document_history": "",
|
||||
"limited_offer": "",
|
||||
"limited_to_n_collaborators_per_project": "",
|
||||
"limited_to_n_collaborators_per_project_plural": "",
|
||||
"line": "",
|
||||
@@ -1764,7 +1762,6 @@
|
||||
"tag_name_cannot_exceed_characters": "",
|
||||
"tag_name_is_already_used": "",
|
||||
"tags": "",
|
||||
"take_short_survey": "",
|
||||
"take_survey": "",
|
||||
"tell_the_project_owner_and_ask_them_to_upgrade": "",
|
||||
"template": "",
|
||||
|
||||
@@ -10,7 +10,6 @@ import EmailsRow from './emails/row'
|
||||
import AddEmail from './emails/add-email'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
import { LeaversSurveyAlert } from './leavers-survey-alert'
|
||||
|
||||
function EmailsSectionContent() {
|
||||
const { t } = useTranslation()
|
||||
@@ -76,7 +75,6 @@ function EmailsSectionContent() {
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{isInitializingSuccess && <LeaversSurveyAlert />}
|
||||
{isInitializingSuccess && !hideAddSecondaryEmail && <AddEmail />}
|
||||
{isInitializingError && (
|
||||
<OLNotification
|
||||
|
||||
@@ -53,8 +53,7 @@ function MakePrimary({
|
||||
}: MakePrimaryProps) {
|
||||
const [show, setShow] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const { state, makePrimary, deleteEmail, resetLeaversSurveyExpiration } =
|
||||
useUserEmailsContext()
|
||||
const { state, makePrimary, deleteEmail } = useUserEmailsContext()
|
||||
|
||||
const handleShowModal = () => setShow(true)
|
||||
const handleHideModal = () => setShow(false)
|
||||
@@ -76,7 +75,6 @@ function MakePrimary({
|
||||
makePrimary(userEmailData.email)
|
||||
if (primary && !primary.confirmedAt) {
|
||||
deleteEmail(primary.email)
|
||||
resetLeaversSurveyExpiration(primary)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
@@ -37,8 +37,7 @@ type RemoveProps = {
|
||||
|
||||
function Remove({ userEmailData, deleteEmailAsync }: RemoveProps) {
|
||||
const { t } = useTranslation()
|
||||
const { state, deleteEmail, resetLeaversSurveyExpiration, setLoading } =
|
||||
useUserEmailsContext()
|
||||
const { state, deleteEmail, setLoading } = useUserEmailsContext()
|
||||
const isManaged = getMeta('ol-isManagedAccount')
|
||||
|
||||
const getTooltipText = () => {
|
||||
@@ -61,7 +60,6 @@ function Remove({ userEmailData, deleteEmailAsync }: RemoveProps) {
|
||||
)
|
||||
.then(() => {
|
||||
deleteEmail(userEmailData.email)
|
||||
resetLeaversSurveyExpiration(userEmailData)
|
||||
// Reset the global loading state before this row is unmounted
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import { useUserEmailsContext } from '../context/user-email-context'
|
||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||
import OLNotification from '@/shared/components/ol/ol-notification'
|
||||
|
||||
function sendMetrics(segmentation: 'view' | 'click' | 'close') {
|
||||
sendMB('institutional-leavers-survey-notification', { type: segmentation })
|
||||
}
|
||||
|
||||
export function LeaversSurveyAlert() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
showInstitutionalLeaversSurveyUntil,
|
||||
setShowInstitutionalLeaversSurveyUntil,
|
||||
} = useUserEmailsContext()
|
||||
|
||||
const [hide, setHide] = usePersistedState(
|
||||
'hideInstitutionalLeaversSurvey',
|
||||
false,
|
||||
{ listen: true }
|
||||
)
|
||||
|
||||
function handleDismiss() {
|
||||
setShowInstitutionalLeaversSurveyUntil(0)
|
||||
setHide(true)
|
||||
sendMetrics('close')
|
||||
}
|
||||
|
||||
function handleLinkClick() {
|
||||
sendMetrics('click')
|
||||
}
|
||||
|
||||
const shouldDisplay =
|
||||
!hide && Date.now() <= showInstitutionalLeaversSurveyUntil
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldDisplay) {
|
||||
sendMetrics('view')
|
||||
}
|
||||
}, [shouldDisplay])
|
||||
|
||||
if (!shouldDisplay) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<OLNotification
|
||||
type="info"
|
||||
content={
|
||||
<>
|
||||
<strong>{t('limited_offer')}</strong>
|
||||
{`: ${t('institutional_leavers_survey_notification')} `}
|
||||
<a
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSfYdeeoY5p1d31r5iUx1jw0O-Gd66vcsBi_Ntu3lJRMjV2EJA/viewform"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
{t('take_short_survey')}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
isDismissible
|
||||
onDismiss={handleDismiss}
|
||||
className="mb-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -13,9 +13,6 @@ import { Affiliation } from '../../../../../types/affiliation'
|
||||
import { normalize, NormalizedObject } from '../../../utils/normalize'
|
||||
import { getJSON } from '../../../infrastructure/fetch-json'
|
||||
import useAsync from '../../../shared/hooks/use-async'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
|
||||
const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export enum Actions {
|
||||
@@ -221,12 +218,6 @@ const reducer = (state: State, action: Action) => {
|
||||
}
|
||||
|
||||
function useUserEmails() {
|
||||
const [
|
||||
showInstitutionalLeaversSurveyUntil,
|
||||
setShowInstitutionalLeaversSurveyUntil,
|
||||
] = usePersistedState('showInstitutionalLeaversSurveyUntil', 0, {
|
||||
listen: true,
|
||||
})
|
||||
const [state, unsafeDispatch] = useReducer(reducer, initialState)
|
||||
const dispatch = useSafeDispatch(unsafeDispatch)
|
||||
const { data, isLoading, isError, isSuccess, runAsync } =
|
||||
@@ -247,34 +238,12 @@ function useUserEmails() {
|
||||
getEmails()
|
||||
}, [getEmails])
|
||||
|
||||
const resetLeaversSurveyExpiration = useCallback(
|
||||
(deletedEmail: UserEmailData) => {
|
||||
if (
|
||||
deletedEmail.emailHasInstitutionLicence ||
|
||||
deletedEmail.affiliation?.pastReconfirmDate
|
||||
) {
|
||||
const stillHasLicenseAccess = Object.values(state.data.byId).some(
|
||||
userEmail =>
|
||||
userEmail.email !== deletedEmail.email &&
|
||||
userEmail.emailHasInstitutionLicence
|
||||
)
|
||||
if (!stillHasLicenseAccess) {
|
||||
setShowInstitutionalLeaversSurveyUntil(Date.now() + ONE_WEEK_IN_MS)
|
||||
}
|
||||
}
|
||||
},
|
||||
[state, setShowInstitutionalLeaversSurveyUntil]
|
||||
)
|
||||
|
||||
return {
|
||||
state,
|
||||
isInitializing: isLoading && !data,
|
||||
isInitializingSuccess: isSuccess,
|
||||
isInitializingError: isError,
|
||||
getEmails,
|
||||
showInstitutionalLeaversSurveyUntil,
|
||||
setShowInstitutionalLeaversSurveyUntil,
|
||||
resetLeaversSurveyExpiration,
|
||||
setLoading: useCallback(
|
||||
(flag: boolean) => dispatch(ActionCreators.setLoading(flag)),
|
||||
[dispatch]
|
||||
|
||||
@@ -200,10 +200,6 @@ export function setDefaultMeta() {
|
||||
hasSamlFeature: true,
|
||||
samlInitPath: 'saml/init',
|
||||
})
|
||||
localStorage.setItem(
|
||||
'showInstitutionalLeaversSurveyUntil',
|
||||
(Date.now() - 1000 * 60 * 60).toString()
|
||||
)
|
||||
}
|
||||
|
||||
export function setReconfirmationMeta() {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import EmailsSection from '../../js/features/settings/components/emails-section'
|
||||
import { UserEmailsProvider } from '../../js/features/settings/context/user-email-context'
|
||||
import { LeaversSurveyAlert } from '../../js/features/settings/components/leavers-survey-alert'
|
||||
import localStorage from '@/infrastructure/local-storage'
|
||||
|
||||
export const SurveyAlert = () => {
|
||||
localStorage.setItem(
|
||||
'showInstitutionalLeaversSurveyUntil',
|
||||
Date.now() + 1000 * 60 * 60
|
||||
)
|
||||
return (
|
||||
<UserEmailsProvider>
|
||||
<LeaversSurveyAlert />
|
||||
</UserEmailsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Account Settings / Survey Alerts',
|
||||
component: EmailsSection,
|
||||
}
|
||||
@@ -1097,7 +1097,6 @@
|
||||
"institution_has_overleaf_subscription": "<0>__institutionName__</0> has an Overleaf subscription. Click the confirmation link sent to __emailAddress__ to upgrade to <0>Overleaf Professional</0>.",
|
||||
"institution_templates": "Institution Templates",
|
||||
"institutional": "Institutional",
|
||||
"institutional_leavers_survey_notification": "Provide some quick feedback to receive a 25% discount on an annual subscription!",
|
||||
"institutional_login_unknown": "Sorry, we don’t know which institution issued that email address. You can browse our <a href=\"__link__\">list of institutions</a> to find yours, or you can use one of the other options below.",
|
||||
"integrations": "Integrations",
|
||||
"integrations_like_github": "Integrations like GitHub Sync",
|
||||
@@ -1232,7 +1231,6 @@
|
||||
"license": "License",
|
||||
"licenses": "Licenses",
|
||||
"limited_document_history": "Limited document history",
|
||||
"limited_offer": "Limited offer",
|
||||
"limited_to_n_collaborators_per_project": "Limited to __count__ collaborator per project",
|
||||
"limited_to_n_collaborators_per_project_plural": "Limited to __count__ collaborators per project",
|
||||
"line_height": "Line Height",
|
||||
@@ -2267,7 +2265,6 @@
|
||||
"tag_name_is_already_used": "Tag \"__tagName__\" already exists",
|
||||
"tags": "Tags",
|
||||
"take_me_home": "Take me home!",
|
||||
"take_short_survey": "Take a short survey",
|
||||
"take_survey": "Take survey",
|
||||
"tell_the_project_owner_and_ask_them_to_upgrade": "<0>Tell the project owner</0> and ask them to upgrade their Overleaf plan if you need more compile time.",
|
||||
"template": "Template",
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { fireEvent, screen, render } from '@testing-library/react'
|
||||
import { UserEmailsProvider } from '../../../../../frontend/js/features/settings/context/user-email-context'
|
||||
import { LeaversSurveyAlert } from '../../../../../frontend/js/features/settings/components/leavers-survey-alert'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import localStorage from '@/infrastructure/local-storage'
|
||||
import fetchMock from 'fetch-mock'
|
||||
|
||||
function renderWithProvider() {
|
||||
render(<LeaversSurveyAlert />, {
|
||||
wrapper: ({ children }) => (
|
||||
<UserEmailsProvider>{children}</UserEmailsProvider>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
describe('<LeaversSurveyAlert/>', function () {
|
||||
beforeEach(function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
it('should render before the expiration date', function () {
|
||||
const tomorrow = Date.now() + 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', tomorrow)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', false)
|
||||
renderWithProvider()
|
||||
screen.getByRole('alert')
|
||||
screen.getByText(/Provide some quick feedback/)
|
||||
screen.getByRole('link', { name: 'Take a short survey' })
|
||||
})
|
||||
|
||||
it('should not render after the expiration date', function () {
|
||||
const yesterday = Date.now() - 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', yesterday)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', false)
|
||||
renderWithProvider()
|
||||
expect(screen.queryByRole('alert')).to.be.null
|
||||
})
|
||||
|
||||
it('should not render if it has been hidden', function () {
|
||||
const tomorrow = Date.now() + 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', tomorrow)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', true)
|
||||
renderWithProvider()
|
||||
expect(screen.queryByRole('alert')).to.be.null
|
||||
})
|
||||
|
||||
it('should reset the expiration date when it is closed', function () {
|
||||
const tomorrow = Date.now() + 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', tomorrow)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', false)
|
||||
renderWithProvider()
|
||||
screen.getByRole('alert')
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(screen.queryByRole('alert')).to.be.null
|
||||
|
||||
expect(localStorage.getItem('showInstitutionalLeaversSurveyUntil')).to.be
|
||||
.null
|
||||
})
|
||||
|
||||
describe('event tracking', function () {
|
||||
let sendMBSpy: sinon.SinonSpy
|
||||
|
||||
beforeEach(function () {
|
||||
sendMBSpy = sinon.spy(eventTracking, 'sendMB')
|
||||
const tomorrow = Date.now() + 1000 * 60 * 60 * 24
|
||||
localStorage.setItem('showInstitutionalLeaversSurveyUntil', tomorrow)
|
||||
localStorage.setItem('hideInstitutionalLeaversSurvey', false)
|
||||
renderWithProvider()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
sendMBSpy.restore()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('should sent a `view` event on load', function () {
|
||||
expect(sendMBSpy).to.be.calledOnce
|
||||
expect(sendMBSpy).calledWith(
|
||||
'institutional-leavers-survey-notification',
|
||||
{ type: 'view', page: '/' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should sent a `click` event when the link is clicked', function () {
|
||||
fireEvent.click(screen.getByRole('link'))
|
||||
expect(sendMBSpy).to.be.calledTwice
|
||||
expect(sendMBSpy).calledWith(
|
||||
'institutional-leavers-survey-notification',
|
||||
{ type: 'click', page: '/' }
|
||||
)
|
||||
})
|
||||
|
||||
it('should sent a `close` event when it is closed', function () {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(sendMBSpy).to.be.calledTwice
|
||||
expect(sendMBSpy).calledWith(
|
||||
'institutional-leavers-survey-notification',
|
||||
{ type: 'close', page: '/' }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
unconfirmedCommonsUserData,
|
||||
untrustedUserData,
|
||||
} from '../fixtures/test-user-email-data'
|
||||
import localStorage from '@/infrastructure/local-storage'
|
||||
|
||||
const renderUserEmailsContext = () =>
|
||||
renderHook(() => useUserEmailsContext(), {
|
||||
@@ -276,120 +275,5 @@ describe('UserEmailContext', function () {
|
||||
expect(result.current.state.data.byId).to.deep.equal(emails)
|
||||
})
|
||||
})
|
||||
|
||||
describe('resetLeaversSurveyExpiration()', function () {
|
||||
beforeEach(function () {
|
||||
localStorage.removeItem('showInstitutionalLeaversSurveyUntil')
|
||||
})
|
||||
|
||||
it('when the leaver has institution license, and there is another email with institution license, it should not reset the survey expiration date', async function () {
|
||||
const affiliatedEmail1 = cloneDeep(professionalUserData)
|
||||
affiliatedEmail1.email = 'institution-test@example.com'
|
||||
affiliatedEmail1.emailHasInstitutionLicence = true
|
||||
|
||||
const affiliatedEmail2 = cloneDeep(professionalUserData)
|
||||
affiliatedEmail2.emailHasInstitutionLicence = true
|
||||
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.flush(true)
|
||||
|
||||
// `resetLeaversSurveyExpiration` always happens after deletion
|
||||
result.current.deleteEmail(affiliatedEmail1.email)
|
||||
result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
|
||||
|
||||
const expiration = localStorage.getItem(
|
||||
'showInstitutionalLeaversSurveyUntil'
|
||||
) as number
|
||||
expect(expiration).to.be.null
|
||||
})
|
||||
|
||||
it("when the leaver's affiliation is past reconfirmation date, and there is another email with institution license, it should not reset the survey expiration date", async function () {
|
||||
const affiliatedEmail1 = cloneDeep(professionalUserData)
|
||||
affiliatedEmail1.email = 'institution-test@example.com'
|
||||
affiliatedEmail1.affiliation.pastReconfirmDate = true
|
||||
|
||||
const affiliatedEmail2 = cloneDeep(professionalUserData)
|
||||
affiliatedEmail2.emailHasInstitutionLicence = true
|
||||
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.flush(true)
|
||||
|
||||
// `resetLeaversSurveyExpiration` always happens after deletion
|
||||
result.current.deleteEmail(affiliatedEmail1.email)
|
||||
result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
|
||||
|
||||
const expiration = localStorage.getItem(
|
||||
'showInstitutionalLeaversSurveyUntil'
|
||||
) as number
|
||||
expect(expiration).to.be.null
|
||||
})
|
||||
|
||||
it('when there are no other emails with institution license, it should reset the survey expiration date', async function () {
|
||||
const affiliatedEmail1 = cloneDeep(professionalUserData)
|
||||
affiliatedEmail1.emailHasInstitutionLicence = true
|
||||
affiliatedEmail1.email = 'institution-test@example.com'
|
||||
affiliatedEmail1.affiliation.pastReconfirmDate = true
|
||||
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [confirmedUserData, affiliatedEmail1])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.flush(true)
|
||||
|
||||
// `resetLeaversSurveyExpiration` always happens after deletion
|
||||
result.current.deleteEmail(affiliatedEmail1.email)
|
||||
result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
localStorage.getItem('showInstitutionalLeaversSurveyUntil')
|
||||
).to.be.greaterThan(Date.now())
|
||||
)
|
||||
})
|
||||
|
||||
it("when the leaver has no institution license, it shouldn't reset the survey expiration date", async function () {
|
||||
const emailWithInstitutionLicense = cloneDeep(professionalUserData)
|
||||
emailWithInstitutionLicense.email = 'institution-licensed@example.com'
|
||||
emailWithInstitutionLicense.emailHasInstitutionLicence = false
|
||||
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.flush(true)
|
||||
|
||||
// `resetLeaversSurveyExpiration` always happens after deletion
|
||||
result.current.deleteEmail(emailWithInstitutionLicense.email)
|
||||
result.current.resetLeaversSurveyExpiration(professionalUserData)
|
||||
|
||||
expect(localStorage.getItem('showInstitutionalLeaversSurveyUntil')).to
|
||||
.be.null
|
||||
})
|
||||
|
||||
it("when the leaver is not past its reconfirmation date, it shouldn't reset the survey expiration date", async function () {
|
||||
const emailWithInstitutionLicense = cloneDeep(professionalUserData)
|
||||
emailWithInstitutionLicense.email = 'institution-licensed@example.com'
|
||||
emailWithInstitutionLicense.affiliation.pastReconfirmDate = false
|
||||
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
|
||||
|
||||
result.current.getEmails()
|
||||
await fetchMock.callHistory.flush(true)
|
||||
|
||||
// `resetLeaversSurveyExpiration` always happens after deletion
|
||||
result.current.deleteEmail(emailWithInstitutionLicense.email)
|
||||
result.current.resetLeaversSurveyExpiration(professionalUserData)
|
||||
|
||||
expect(localStorage.getItem('showInstitutionalLeaversSurveyUntil')).to
|
||||
.be.null
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user