mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
Merge pull request #20371 from overleaf/mf-us-gov-banner
[web] Add US gov banner based on inclusion and exclusion criteria GitOrigin-RevId: c45ed280c8ef2dbdf9f3b84488e767c06fcc1ae1
This commit is contained in:
parent
f3cb79c12f
commit
16ba4b0ddf
12 changed files with 211 additions and 11 deletions
|
@ -26,6 +26,7 @@ const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
|
|||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
const SplitTestSessionHandler = require('../SplitTests/SplitTestSessionHandler')
|
||||
const SubscriptionLocator = require('../Subscription/SubscriptionLocator')
|
||||
const TutorialHandler = require('../Tutorial/TutorialHandler')
|
||||
|
||||
/**
|
||||
* @import { GetProjectsRequest, GetProjectsResponse, AllUsersProjects, MongoProject } from "./types"
|
||||
|
@ -114,7 +115,7 @@ async function projectListPage(req, res, next) {
|
|||
const user = await User.findById(
|
||||
userId,
|
||||
`email emails features alphaProgram betaProgram lastPrimaryEmailCheck labsProgram signUpDate${
|
||||
isSaas ? ' enrollment writefull' : ''
|
||||
isSaas ? ' enrollment writefull completedTutorials' : ''
|
||||
}`
|
||||
)
|
||||
|
||||
|
@ -351,8 +352,26 @@ async function projectListPage(req, res, next) {
|
|||
affiliation => affiliation.licence && affiliation.licence !== 'free'
|
||||
)
|
||||
|
||||
const inactiveTutorials = TutorialHandler.getInactiveTutorials(user)
|
||||
|
||||
const usGovBannerHooksResponse = await Modules.promises.hooks.fire(
|
||||
'getUSGovBanner',
|
||||
userEmails,
|
||||
hasPaidAffiliation,
|
||||
inactiveTutorials.includes('us-gov-banner')
|
||||
)
|
||||
|
||||
const usGovBanner = (usGovBannerHooksResponse &&
|
||||
usGovBannerHooksResponse[0]) || {
|
||||
showUSGovBanner: false,
|
||||
usGovBannerVariant: null,
|
||||
}
|
||||
|
||||
const { showUSGovBanner, usGovBannerVariant } = usGovBanner
|
||||
|
||||
const showGroupsAndEnterpriseBanner =
|
||||
Features.hasFeature('saas') &&
|
||||
!showUSGovBanner &&
|
||||
!userIsMemberOfGroupSubscription &&
|
||||
!hasPaidAffiliation
|
||||
|
||||
|
@ -455,6 +474,8 @@ async function projectListPage(req, res, next) {
|
|||
prefetchedProjectsBlob,
|
||||
showGroupsAndEnterpriseBanner,
|
||||
groupsAndEnterpriseBannerVariant,
|
||||
showUSGovBanner,
|
||||
usGovBannerVariant,
|
||||
showWritefullPromoBanner,
|
||||
showLATAMBanner,
|
||||
recommendedCurrency,
|
||||
|
|
|
@ -10,6 +10,7 @@ const VALID_KEYS = [
|
|||
'ai-error-assistant-consent',
|
||||
'code-editor-mode-prompt',
|
||||
'history-restore-promo',
|
||||
'us-gov-banner',
|
||||
]
|
||||
|
||||
async function completeTutorial(req, res, next) {
|
||||
|
@ -27,12 +28,21 @@ async function completeTutorial(req, res, next) {
|
|||
async function postponeTutorial(req, res, next) {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
const tutorialKey = req.params.tutorialKey
|
||||
let postponedUntil
|
||||
if (req.body.postponedUntil) {
|
||||
postponedUntil = new Date(req.body.postponedUntil)
|
||||
}
|
||||
|
||||
if (!VALID_KEYS.includes(tutorialKey)) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
await TutorialHandler.setTutorialState(userId, tutorialKey, 'postponed')
|
||||
await TutorialHandler.setTutorialState(
|
||||
userId,
|
||||
tutorialKey,
|
||||
'postponed',
|
||||
postponedUntil
|
||||
)
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,26 @@ const POSTPONE_DURATION_MS = 24 * 60 * 60 * 1000 // 1 day
|
|||
* @param {string} userId
|
||||
* @param {string} tutorialKey
|
||||
* @param {'completed' | 'postponed'} state
|
||||
* @param {Date} [postponedUntil] - The date until which the tutorial is postponed
|
||||
*/
|
||||
async function setTutorialState(userId, tutorialKey, state) {
|
||||
async function setTutorialState(
|
||||
userId,
|
||||
tutorialKey,
|
||||
state,
|
||||
postponedUntil = null
|
||||
) {
|
||||
const updateData = {
|
||||
state,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
if (state === 'postponed' && postponedUntil) {
|
||||
updateData.postponedUntil = postponedUntil
|
||||
}
|
||||
|
||||
await UserUpdater.promises.updateUser(userId, {
|
||||
$set: {
|
||||
[`completedTutorials.${tutorialKey}`]: { state, updatedAt: new Date() },
|
||||
[`completedTutorials.${tutorialKey}`]: updateData,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -29,9 +44,11 @@ function getInactiveTutorials(user, tutorialKey) {
|
|||
// Legacy format: single date means the tutorial was completed
|
||||
inactiveTutorials.push(key)
|
||||
} else if (record.state === 'postponed') {
|
||||
const postponedUntil = new Date(
|
||||
const defaultPostponedUntil = new Date(
|
||||
record.updatedAt.getTime() + POSTPONE_DURATION_MS
|
||||
)
|
||||
|
||||
const postponedUntil = record.postponedUntil ?? defaultPostponedUntil
|
||||
if (new Date() < postponedUntil) {
|
||||
inactiveTutorials.push(key)
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@ block append meta
|
|||
meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
|
||||
meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)
|
||||
meta(name="ol-groupSsoSetupSuccess" data-type="boolean" content=groupSsoSetupSuccess)
|
||||
meta(name="ol-showUSGovBanner" data-type="boolean" content=showUSGovBanner)
|
||||
meta(name="ol-usGovBannerVariant" data-type="string" content=usGovBannerVariant)
|
||||
|
||||
block content
|
||||
main.content.content-alt.project-list-react#main-content
|
||||
|
|
|
@ -959,6 +959,7 @@ module.exports = {
|
|||
ssoCertificateInfo: [],
|
||||
v1ImportDataScreen: [],
|
||||
snapshotUtils: [],
|
||||
usGovBanner: [],
|
||||
offlineModeToolbarButtons: [],
|
||||
settingsEntries: [],
|
||||
},
|
||||
|
|
|
@ -1687,6 +1687,8 @@
|
|||
"upload_project": "",
|
||||
"upload_zipped_project": "",
|
||||
"url_to_fetch_the_file_from": "",
|
||||
"us_gov_banner_government_purchasing": "",
|
||||
"us_gov_banner_small_business_reseller": "",
|
||||
"use_a_different_password": "",
|
||||
"use_saml_metadata_to_configure_sso_with_idp": "",
|
||||
"use_your_own_machine": "",
|
||||
|
|
|
@ -17,6 +17,8 @@ const [enrollmentNotificationModule] = importOverleafModules(
|
|||
'managedGroupSubscriptionEnrollmentNotification'
|
||||
)
|
||||
|
||||
const [usGovBannerModule] = importOverleafModules('usGovBanner')
|
||||
|
||||
const moduleNotifications = importOverleafModules('userNotifications') as {
|
||||
import: { default: ElementType }
|
||||
path: string
|
||||
|
@ -27,6 +29,9 @@ const EnrollmentNotification: JSXElementConstructor<{
|
|||
groupName: string
|
||||
}> = enrollmentNotificationModule?.import.default
|
||||
|
||||
const USGovBanner: JSXElementConstructor<Record<string, never>> =
|
||||
usGovBannerModule?.import.default
|
||||
|
||||
function UserNotifications() {
|
||||
const groupSubscriptionsPendingEnrollment =
|
||||
getMeta('ol-groupSubscriptionsPendingEnrollment') || []
|
||||
|
@ -75,6 +80,7 @@ function UserNotifications() {
|
|||
<ReconfirmationInfo />
|
||||
<GeoBanners />
|
||||
{!showWritefull && !dismissedWritefull && <GroupsAndEnterpriseBanner />}
|
||||
<USGovBanner />
|
||||
|
||||
<AccessibilitySurveyBanner />
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
Institution as InstitutionType,
|
||||
Notification as NotificationType,
|
||||
PendingGroupSubscriptionEnrollment,
|
||||
USGovBannerVariant,
|
||||
} from '../../../types/project/dashboard/notification'
|
||||
import { Survey } from '../../../types/project/dashboard/survey'
|
||||
import { GetProjectsResponseBody } from '../../../types/project/dashboard/api'
|
||||
|
@ -180,6 +181,7 @@ export interface Meta {
|
|||
'ol-showSupport': boolean
|
||||
'ol-showSymbolPalette': boolean
|
||||
'ol-showTemplatesServerPro': boolean
|
||||
'ol-showUSGovBanner': boolean
|
||||
'ol-showUpgradePrompt': boolean
|
||||
'ol-skipUrl': string
|
||||
'ol-splitTestInfo': { [name: string]: SplitTestInfo }
|
||||
|
@ -198,6 +200,7 @@ export interface Meta {
|
|||
'ol-translationLoadErrorMessage': string
|
||||
'ol-translationMaintenance': string
|
||||
'ol-translationUnableToJoin': string
|
||||
'ol-usGovBannerVariant': USGovBannerVariant
|
||||
'ol-useShareJsHash': boolean
|
||||
'ol-usedLatex': 'never' | 'occasionally' | 'often' | undefined
|
||||
'ol-user': User
|
||||
|
|
|
@ -2300,6 +2300,8 @@
|
|||
"upload_project": "Upload Project",
|
||||
"upload_zipped_project": "Upload Zipped Project",
|
||||
"url_to_fetch_the_file_from": "URL to fetch the file from",
|
||||
"us_gov_banner_government_purchasing": "<0>Get __appName__ for US federal government. </0>Move faster through procurement with our tailored purchasing options. Talk to our government team.",
|
||||
"us_gov_banner_small_business_reseller": "<0>Easy procurement for US federal government. </0>We partner with small business resellers to help you buy Overleaf organizational plans. Talk to our government team.",
|
||||
"usage_metrics": "Usage metrics",
|
||||
"usage_metrics_info": "Metrics that show how many users are accessing the licence, how many projects are being created and worked on, and how much collaboration is happening in Overleaf.",
|
||||
"use_a_different_password": "Please use a different password",
|
||||
|
|
|
@ -138,6 +138,17 @@ describe('ProjectListController', function () {
|
|||
}),
|
||||
},
|
||||
}
|
||||
this.TutorialHandler = {
|
||||
getInactiveTutorials: sinon.stub().returns([]),
|
||||
}
|
||||
|
||||
this.Modules = {
|
||||
promises: {
|
||||
hooks: {
|
||||
fire: sinon.stub().resolves([]),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
this.ProjectListController = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
|
@ -158,17 +169,14 @@ describe('ProjectListController', function () {
|
|||
'../User/UserGetter': this.UserGetter,
|
||||
'../Subscription/SubscriptionViewModelBuilder':
|
||||
this.SubscriptionViewModelBuilder,
|
||||
'../../infrastructure/Modules': {
|
||||
promises: {
|
||||
hooks: { fire: sinon.stub().resolves([]) },
|
||||
},
|
||||
},
|
||||
'../../infrastructure/Modules': this.Modules,
|
||||
'../Survey/SurveyHandler': this.SurveyHandler,
|
||||
'../User/UserPrimaryEmailCheckHandler':
|
||||
this.UserPrimaryEmailCheckHandler,
|
||||
'../Notifications/NotificationsBuilder': this.NotificationBuilder,
|
||||
'../Subscription/SubscriptionLocator': this.SubscriptionLocator,
|
||||
'../../infrastructure/GeoIpLookup': this.GeoIpLookup,
|
||||
'../Tutorial/TutorialHandler': this.TutorialHandler,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -594,6 +602,101 @@ describe('ProjectListController', function () {
|
|||
this.ProjectListController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('enterprise banner', function () {
|
||||
beforeEach(function (done) {
|
||||
this.Features.hasFeature.withArgs('saas').returns(true)
|
||||
this.LimitationsManager.promises.userIsMemberOfGroupSubscription.resolves(
|
||||
{ isMember: false }
|
||||
)
|
||||
this.UserGetter.promises.getUserFullEmails.resolves([
|
||||
{
|
||||
email: 'test@test-domain.com',
|
||||
},
|
||||
])
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
describe('normal enterprise banner', function () {
|
||||
it('shows banner', function () {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showGroupsAndEnterpriseBanner).to.be.true
|
||||
}
|
||||
this.ProjectListController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('does not show banner if user is part of any affiliation', function () {
|
||||
this.UserGetter.promises.getUserFullEmails.resolves([
|
||||
{
|
||||
email: 'test@overleaf.com',
|
||||
affiliation: {
|
||||
licence: 'pro_plus',
|
||||
institution: {
|
||||
id: 1,
|
||||
confirmed: true,
|
||||
name: 'Overleaf',
|
||||
ssoBeta: false,
|
||||
ssoEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showGroupsAndEnterpriseBanner).to.be.false
|
||||
}
|
||||
this.ProjectListController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('does not show banner if user is part of any group subscription', function () {
|
||||
this.LimitationsManager.promises.userIsMemberOfGroupSubscription.resolves(
|
||||
{ isMember: true }
|
||||
)
|
||||
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showGroupsAndEnterpriseBanner).to.be.false
|
||||
}
|
||||
this.ProjectListController.projectListPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('have a banner variant of "FOMO" or "on-premise"', function () {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.groupsAndEnterpriseBannerVariant).to.be.oneOf([
|
||||
'FOMO',
|
||||
'on-premise',
|
||||
])
|
||||
}
|
||||
this.ProjectListController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('US government enterprise banner', function () {
|
||||
it('does not show enterprise banner if US government enterprise banner is shown', function () {
|
||||
const emails = [
|
||||
{
|
||||
email: 'test@test.mil',
|
||||
confirmedAt: new Date('2024-01-01'),
|
||||
},
|
||||
]
|
||||
|
||||
this.UserGetter.promises.getUserFullEmails.resolves(emails)
|
||||
this.Modules.promises.hooks.fire
|
||||
.withArgs('getUSGovBanner', emails, false, false)
|
||||
.resolves([
|
||||
{
|
||||
showUSGovBanner: true,
|
||||
usGovBannerVariant: 'variant',
|
||||
},
|
||||
])
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showGroupsAndEnterpriseBanner).to.be.false
|
||||
expect(opts.showUSGovBanner).to.be.true
|
||||
}
|
||||
this.ProjectListController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectListReactPage with duplicate projects', function () {
|
||||
|
|
|
@ -9,6 +9,10 @@ describe('TutorialHandler', function () {
|
|||
beforeEach(function () {
|
||||
this.clock = sinon.useFakeTimers()
|
||||
|
||||
const THIRTY_DAYS_AGO = Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||
const TOMORROW = Date.now() + 24 * 60 * 60 * 1000
|
||||
const YESTERDAY = Date.now() - 24 * 60 * 60 * 1000
|
||||
|
||||
this.user = {
|
||||
_id: new ObjectId(),
|
||||
completedTutorials: {
|
||||
|
@ -23,7 +27,17 @@ describe('TutorialHandler', function () {
|
|||
},
|
||||
'postponed-long-ago': {
|
||||
state: 'postponed',
|
||||
updatedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||
updatedAt: new Date(THIRTY_DAYS_AGO),
|
||||
},
|
||||
'postponed-until-tomorrow': {
|
||||
state: 'postponed',
|
||||
updatedAt: new Date(THIRTY_DAYS_AGO),
|
||||
postponedUntil: new Date(TOMORROW),
|
||||
},
|
||||
'postponed-until-yesterday': {
|
||||
state: 'postponed',
|
||||
updatedAt: new Date(THIRTY_DAYS_AGO),
|
||||
postponedUntil: new Date(YESTERDAY),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -54,7 +68,20 @@ describe('TutorialHandler', function () {
|
|||
'legacy-format',
|
||||
'completed',
|
||||
'postponed-recently',
|
||||
'postponed-until-tomorrow',
|
||||
])
|
||||
|
||||
expect(hiddenTutorials).to.have.lengthOf(4)
|
||||
|
||||
const shownTutorials = Object.keys(this.user.completedTutorials).filter(
|
||||
key => !hiddenTutorials.includes(key)
|
||||
)
|
||||
|
||||
expect(shownTutorials).to.have.members([
|
||||
'postponed-long-ago',
|
||||
'postponed-until-yesterday',
|
||||
])
|
||||
expect(shownTutorials).to.have.lengthOf(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -105,3 +105,9 @@ export type PendingGroupSubscriptionEnrollment = {
|
|||
export const GroupsAndEnterpriseBannerVariants = ['on-premise', 'FOMO'] as const
|
||||
export type GroupsAndEnterpriseBannerVariant =
|
||||
(typeof GroupsAndEnterpriseBannerVariants)[number]
|
||||
|
||||
export const USGovBannerVariants = [
|
||||
'government-purchasing',
|
||||
'small-business-reseller',
|
||||
] as const
|
||||
export type USGovBannerVariant = (typeof USGovBannerVariants)[number]
|
||||
|
|
Loading…
Reference in a new issue