mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 13:03:42 -05:00
Merge pull request #13804 from overleaf/tm-geopricing-inr-banners
Implement split test for new INR geo-pricing banners/modal GitOrigin-RevId: 06fbcf70b7ee90b9b365ac96c1fa0373cbe60847
This commit is contained in:
parent
780e5fdca5
commit
e704afdcca
12 changed files with 299 additions and 75 deletions
|
@ -363,21 +363,24 @@ async function projectListPage(req, res, next) {
|
|||
}
|
||||
}
|
||||
|
||||
let showINRBanner = false
|
||||
let showInrGeoBanner, inrGeoBannerSplitTestName
|
||||
let inrGeoBannerVariant = 'default'
|
||||
let showLATAMBanner = false
|
||||
let recommendedCurrency
|
||||
if (usersBestSubscription?.type === 'free') {
|
||||
const { currencyCode, countryCode } =
|
||||
await GeoIpLookup.promises.getCurrencyCode(req.ip)
|
||||
let inrGeoPricingVariant = 'default'
|
||||
try {
|
||||
// Split test is kept active, but all users geolocated in India can
|
||||
// now use the INR currency (See #13507)
|
||||
const inrGeoPricingAssignment =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'geo-pricing-inr'
|
||||
)
|
||||
showINRBanner =
|
||||
inrGeoPricingAssignment.variant === 'inr' && countryCode === 'IN'
|
||||
inrGeoPricingVariant = inrGeoPricingAssignment.variant
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error },
|
||||
|
@ -404,6 +407,27 @@ async function projectListPage(req, res, next) {
|
|||
'Failed to get geo-pricing-latam split test assignment'
|
||||
)
|
||||
}
|
||||
if (countryCode === 'IN') {
|
||||
inrGeoBannerSplitTestName =
|
||||
inrGeoPricingVariant === 'inr'
|
||||
? 'geo-banners-inr-2'
|
||||
: 'geo-banners-inr-1'
|
||||
try {
|
||||
const geoBannerAssignment =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
inrGeoBannerSplitTestName
|
||||
)
|
||||
showInrGeoBanner = true
|
||||
inrGeoBannerVariant = geoBannerAssignment.variant
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error },
|
||||
`Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.render('project/list-react', {
|
||||
|
@ -423,9 +447,11 @@ async function projectListPage(req, res, next) {
|
|||
showGroupsAndEnterpriseBanner,
|
||||
groupsAndEnterpriseBannerVariant,
|
||||
showWritefullPromoBanner,
|
||||
showINRBanner,
|
||||
showLATAMBanner,
|
||||
recommendedCurrency,
|
||||
showInrGeoBanner,
|
||||
inrGeoBannerVariant,
|
||||
inrGeoBannerSplitTestName,
|
||||
projectDashboardReact: true, // used in navbar
|
||||
welcomePageRedesignVariant: welcomePageRedesignAssignment.variant,
|
||||
groupSubscriptionsPendingEnrollment:
|
||||
|
|
|
@ -108,7 +108,32 @@ async function plansPage(req, res) {
|
|||
removePersonalPlanAssingment
|
||||
)
|
||||
|
||||
AnalyticsManager.recordEventForSession(req.session, 'plans-page-view', {
|
||||
let showInrGeoBanner, inrGeoBannerSplitTestName
|
||||
let inrGeoBannerVariant = 'default'
|
||||
if (countryCode === 'IN') {
|
||||
inrGeoBannerSplitTestName =
|
||||
geoPricingINRTestVariant === 'inr'
|
||||
? 'geo-banners-inr-2'
|
||||
: 'geo-banners-inr-1'
|
||||
try {
|
||||
const geoBannerAssignment = await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
inrGeoBannerSplitTestName
|
||||
)
|
||||
inrGeoBannerVariant = geoBannerAssignment.variant
|
||||
if (inrGeoBannerVariant !== 'default') {
|
||||
showInrGeoBanner = true
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error },
|
||||
`Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const plansPageViewSegmentation = {
|
||||
currency: recommendedCurrency,
|
||||
'remove-personal-plan-page': removePersonalPlanAssingment?.variant,
|
||||
countryCode,
|
||||
|
@ -120,7 +145,16 @@ async function plansPage(req, res) {
|
|||
)
|
||||
? 'latam'
|
||||
: 'default',
|
||||
})
|
||||
}
|
||||
if (inrGeoBannerSplitTestName) {
|
||||
plansPageViewSegmentation[inrGeoBannerSplitTestName] = inrGeoBannerVariant
|
||||
}
|
||||
|
||||
AnalyticsManager.recordEventForSession(
|
||||
req.session,
|
||||
'plans-page-view',
|
||||
plansPageViewSegmentation
|
||||
)
|
||||
|
||||
res.render(`subscriptions/plans-marketing/${directory}/plans-marketing-v2`, {
|
||||
title: 'plans_and_pricing',
|
||||
|
@ -137,6 +171,7 @@ async function plansPage(req, res) {
|
|||
groupPlanModalDefaults,
|
||||
initialLocalizedGroupPrice:
|
||||
SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
|
||||
showInrGeoBanner,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -302,15 +337,36 @@ async function interstitialPaymentPage(req, res) {
|
|||
if (hasSubscription) {
|
||||
res.redirect('/user/subscription?hasSubscription=true')
|
||||
} else {
|
||||
AnalyticsManager.recordEventForSession(
|
||||
req.session,
|
||||
'paywall-plans-page-view',
|
||||
{
|
||||
let showInrGeoBanner, inrGeoBannerSplitTestName
|
||||
let inrGeoBannerVariant = 'default'
|
||||
if (countryCode === 'IN') {
|
||||
inrGeoBannerSplitTestName =
|
||||
geoPricingINRTestVariant === 'inr'
|
||||
? 'geo-banners-inr-2'
|
||||
: 'geo-banners-inr-1'
|
||||
try {
|
||||
const geoBannerAssignment =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
inrGeoBannerSplitTestName
|
||||
)
|
||||
inrGeoBannerVariant = geoBannerAssignment.variant
|
||||
if (inrGeoBannerVariant !== 'default') {
|
||||
showInrGeoBanner = true
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
{ err: error },
|
||||
`Failed to get INR geo banner lookup or assignment (${inrGeoBannerSplitTestName})`
|
||||
)
|
||||
}
|
||||
}
|
||||
const paywallPlansPageViewSegmentation = {
|
||||
currency: recommendedCurrency,
|
||||
countryCode,
|
||||
'geo-pricing-inr-group': geoPricingINRTestVariant,
|
||||
'geo-pricing-inr-page':
|
||||
recommendedCurrency === 'INR' ? 'inr' : 'default',
|
||||
'geo-pricing-inr-page': recommendedCurrency === 'INR' ? 'inr' : 'default',
|
||||
'geo-pricing-latam-group': geoPricingLATAMTestVariant,
|
||||
'geo-pricing-latam-page': ['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(
|
||||
recommendedCurrency
|
||||
|
@ -319,6 +375,14 @@ async function interstitialPaymentPage(req, res) {
|
|||
: 'default',
|
||||
'remove-personal-plan-page': removePersonalPlanAssingment?.variant,
|
||||
}
|
||||
if (inrGeoBannerSplitTestName) {
|
||||
paywallPlansPageViewSegmentation[inrGeoBannerSplitTestName] =
|
||||
inrGeoBannerVariant
|
||||
}
|
||||
AnalyticsManager.recordEventForSession(
|
||||
req.session,
|
||||
'paywall-plans-page-view',
|
||||
paywallPlansPageViewSegmentation
|
||||
)
|
||||
|
||||
res.render(
|
||||
|
@ -331,6 +395,7 @@ async function interstitialPaymentPage(req, res) {
|
|||
recommendedCurrency,
|
||||
interstitialPaymentConfig,
|
||||
showSkipLink,
|
||||
showInrGeoBanner,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -678,10 +743,12 @@ async function _getRecommendedCurrency(req, res) {
|
|||
req.query?.ip || req.ip
|
||||
)
|
||||
const countryCode = currencyLookup.countryCode
|
||||
let recommendedCurrency = currencyLookup.currencyCode
|
||||
let assignmentINR, assignmentLATAM
|
||||
let recommendedCurrency = currencyLookup.currencyCode
|
||||
// for #12703
|
||||
try {
|
||||
// Split test is kept active, but all users geolocated in India can
|
||||
// now use the INR currency (See #13507)
|
||||
assignmentINR = await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
|
@ -706,11 +773,6 @@ async function _getRecommendedCurrency(req, res) {
|
|||
'Failed to get assignment for geo-pricing-latam test'
|
||||
)
|
||||
}
|
||||
// if the user has been detected as located in India (thus recommended INR as currency)
|
||||
// but is not part of the geo pricing test, we fall back to the default currency instead
|
||||
if (recommendedCurrency === 'INR' && assignmentINR?.variant !== 'inr') {
|
||||
recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
|
||||
}
|
||||
if (
|
||||
['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency) &&
|
||||
assignmentLATAM?.variant !== 'latam'
|
||||
|
|
|
@ -28,7 +28,9 @@ block append meta
|
|||
meta(name="ol-showGroupsAndEnterpriseBanner" data-type="boolean" content=showGroupsAndEnterpriseBanner)
|
||||
meta(name="ol-showWritefullPromoBanner" data-type="boolean" content=showWritefullPromoBanner)
|
||||
meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
|
||||
meta(name="ol-showINRBanner" data-type="boolean" content=showINRBanner)
|
||||
meta(name="ol-showInrGeoBanner" data-type="boolean" content=showInrGeoBanner)
|
||||
meta(name="ol-inrGeoBannerVariant" data-type="string" content=inrGeoBannerVariant)
|
||||
meta(name="ol-inrGeoBannerSplitTestName" data-type="string" content=inrGeoBannerSplitTestName)
|
||||
meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
|
||||
meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency)
|
||||
meta(name="ol-welcomePageRedesignVariant" data-type="string" content=welcomePageRedesignVariant)
|
||||
|
|
|
@ -16,6 +16,9 @@ block content
|
|||
.content-page
|
||||
.plans
|
||||
.container
|
||||
if showInrGeoBanner
|
||||
div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
.page-header.centered.plans-header.text-centered.top-page-header
|
||||
|
|
|
@ -15,6 +15,9 @@ block content
|
|||
.content-page
|
||||
.plans
|
||||
.container(ng-cloak)
|
||||
if showInrGeoBanner
|
||||
div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
.page-header.centered.plans-header.text-centered.top-page-header
|
||||
|
|
|
@ -16,6 +16,9 @@ block content
|
|||
.content-page
|
||||
.plans
|
||||
.container
|
||||
if showInrGeoBanner
|
||||
div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
.page-header.centered.plans-header.text-centered.top-page-header
|
||||
|
|
|
@ -15,6 +15,9 @@ block content
|
|||
.content-page
|
||||
.plans
|
||||
.container(ng-cloak)
|
||||
if showInrGeoBanner
|
||||
div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
|
||||
|
||||
.row
|
||||
.col-md-12
|
||||
.page-header.centered.plans-header.text-centered.top-page-header
|
||||
|
|
|
@ -494,7 +494,10 @@
|
|||
"include_caption": "",
|
||||
"include_label": "",
|
||||
"increased_compile_timeout": "",
|
||||
"inr_discount_modal_info": "",
|
||||
"inr_discount_modal_title": "",
|
||||
"inr_discount_offer": "",
|
||||
"inr_discount_offer_green_banner": "",
|
||||
"insert_figure": "",
|
||||
"insert_from_another_project": "",
|
||||
"insert_from_project_files": "",
|
||||
|
@ -613,6 +616,7 @@
|
|||
"math_display": "",
|
||||
"math_inline": "",
|
||||
"maximum_files_uploaded_together": "",
|
||||
"maybe_later": "",
|
||||
"members_management": "",
|
||||
"mendeley_groups_loading_error": "",
|
||||
"mendeley_groups_relink": "",
|
||||
|
|
|
@ -1,51 +1,102 @@
|
|||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import usePersistedState from '../../../../../shared/hooks/use-persisted-state'
|
||||
import Notification from '../notification'
|
||||
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { Modal, Button } from 'react-bootstrap'
|
||||
import AccessibleModal from '../../../../../shared/components/accessible-modal'
|
||||
|
||||
export default function INRBanner() {
|
||||
interface VariantContents {
|
||||
default: string
|
||||
'green-banner': string
|
||||
modal: string
|
||||
}
|
||||
|
||||
const contentLookup: VariantContents = {
|
||||
default: 'blue',
|
||||
'green-banner': 'green',
|
||||
modal: 'modal',
|
||||
}
|
||||
|
||||
type INRBannerProps = {
|
||||
variant: keyof VariantContents
|
||||
splitTestName: string
|
||||
}
|
||||
|
||||
export default function INRBanner({ variant, splitTestName }: INRBannerProps) {
|
||||
const { t } = useTranslation()
|
||||
const [dismissedAt, setDismissedAt] = usePersistedState<Date | undefined>(
|
||||
`has_dismissed_inr_banner`
|
||||
)
|
||||
const [dismissedUntil, setDismissedUntil] = usePersistedState<
|
||||
Date | undefined
|
||||
>(`has_dismissed_inr_banner_until`)
|
||||
const viewEventSent = useRef<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!dismissedAt) {
|
||||
return
|
||||
}
|
||||
const dismissedAtDate = new Date(dismissedAt)
|
||||
const recentlyDismissedCutoff = new Date()
|
||||
recentlyDismissedCutoff.setDate(recentlyDismissedCutoff.getDate() - 30) // 30 days
|
||||
// once dismissedAt passes the cut-off mark, banner will be shown again
|
||||
if (dismissedAtDate <= recentlyDismissedCutoff) {
|
||||
setDismissedAt(undefined)
|
||||
}
|
||||
}, [dismissedAt, setDismissedAt])
|
||||
// Only used by 'modal' variant
|
||||
const [showModal, setShowModal] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!dismissedAt && !viewEventSent.current) {
|
||||
eventTracking.sendMB('paywall-prompt', {
|
||||
'paywall-type': 'inr-banner',
|
||||
if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
|
||||
return
|
||||
}
|
||||
if (!viewEventSent.current) {
|
||||
eventTracking.sendMB('promo-prompt', {
|
||||
location: variant === 'modal' ? 'dashboard-modal' : 'dashboard-banner',
|
||||
name: splitTestName,
|
||||
content: contentLookup[variant],
|
||||
})
|
||||
viewEventSent.current = true
|
||||
}
|
||||
}, [dismissedAt])
|
||||
}, [dismissedUntil, splitTestName, variant])
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
eventTracking.sendMB('paywall-click', { 'paywall-type': 'inr-banner' })
|
||||
eventTracking.sendMB('promo-click', {
|
||||
location: variant === 'modal' ? 'dashboard-modal' : 'dashboard-banner',
|
||||
name: splitTestName,
|
||||
content: contentLookup[variant],
|
||||
type: 'click',
|
||||
})
|
||||
|
||||
setShowModal(false)
|
||||
|
||||
window.open('/user/subscription/plans')
|
||||
}, [])
|
||||
}, [splitTestName, variant])
|
||||
|
||||
if (dismissedAt) {
|
||||
const bannerDismissed = useCallback(() => {
|
||||
eventTracking.sendMB('promo-dismiss', {
|
||||
location: variant === 'modal' ? 'dashboard-modal' : 'dashboard-banner',
|
||||
name: splitTestName,
|
||||
content: contentLookup[variant],
|
||||
type: 'click',
|
||||
})
|
||||
const until = new Date()
|
||||
until.setDate(until.getDate() + 30) // 30 days
|
||||
setDismissedUntil(until)
|
||||
}, [setDismissedUntil, splitTestName, variant])
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
setShowModal(false)
|
||||
bannerDismissed()
|
||||
}, [bannerDismissed])
|
||||
|
||||
const handleMaybeLater = useCallback(() => {
|
||||
eventTracking.sendMB('promo-click', {
|
||||
location: variant === 'modal' ? 'dashboard-modal' : 'dashboard-banner',
|
||||
name: splitTestName,
|
||||
content: contentLookup[variant],
|
||||
type: 'pause',
|
||||
})
|
||||
setShowModal(false)
|
||||
const until = new Date()
|
||||
until.setDate(until.getDate() + 1) // 1 day
|
||||
setDismissedUntil(until)
|
||||
}, [setDismissedUntil, splitTestName, variant])
|
||||
|
||||
if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (variant === 'default') {
|
||||
return (
|
||||
<Notification bsStyle="info" onDismiss={() => setDismissedAt(new Date())}>
|
||||
<Notification bsStyle="info" onDismiss={bannerDismissed}>
|
||||
<Notification.Body>
|
||||
<Trans
|
||||
i18nKey="inr_discount_offer"
|
||||
|
@ -64,4 +115,58 @@ export default function INRBanner() {
|
|||
</Notification.Action>
|
||||
</Notification>
|
||||
)
|
||||
} else if (variant === 'green-banner') {
|
||||
return (
|
||||
<Notification bsStyle="success" onDismiss={bannerDismissed}>
|
||||
<Notification.Body>
|
||||
<Trans
|
||||
i18nKey="inr_discount_offer_green_banner"
|
||||
components={[<b />, <br />]} // eslint-disable-line react/jsx-key
|
||||
values={{ flag: '🇮🇳' }}
|
||||
/>
|
||||
</Notification.Body>
|
||||
<Notification.Action>
|
||||
<Button
|
||||
bsStyle="success"
|
||||
className="pull-right"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{t('get_discounted_plan')} ₹
|
||||
</Button>
|
||||
</Notification.Action>
|
||||
</Notification>
|
||||
)
|
||||
} else if (variant === 'modal') {
|
||||
return (
|
||||
<AccessibleModal show={showModal} onHide={handleHide}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{t('inr_discount_modal_title')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="modal-body-share">
|
||||
<p>
|
||||
<img
|
||||
alt={t('inr_discount_modal_title')}
|
||||
src="/img/subscriptions/inr-discount-modal.png"
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Trans i18nKey="inr_discount_modal_info" />
|
||||
</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button bsStyle="default" onClick={handleMaybeLater}>
|
||||
{t('maybe_later')}
|
||||
</Button>
|
||||
<Button type="button" bsStyle="primary" onClick={handleClick}>
|
||||
{t('get_discounted_plan')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,12 @@ function UserNotifications() {
|
|||
'ol-groupSubscriptionsPendingEnrollment',
|
||||
[]
|
||||
)
|
||||
const showIRNBanner = getMeta('ol-showINRBanner', false)
|
||||
const showInrGeoBanner = getMeta('ol-showInrGeoBanner', false)
|
||||
const inrGeoBannerVariant = getMeta('ol-inrGeoBannerVariant', 'default')
|
||||
const inrGeoBannerSplitTestName = getMeta(
|
||||
'ol-inrGeoBannerSplitTestName',
|
||||
'unassigned'
|
||||
)
|
||||
const showLATAMBanner = getMeta('ol-showLATAMBanner', false)
|
||||
|
||||
return (
|
||||
|
@ -48,8 +53,11 @@ function UserNotifications() {
|
|||
<ReconfirmationInfo />
|
||||
{showLATAMBanner ? (
|
||||
<LATAMBanner />
|
||||
) : showIRNBanner ? (
|
||||
<INRBanner />
|
||||
) : showInrGeoBanner ? (
|
||||
<INRBanner
|
||||
variant={inrGeoBannerVariant}
|
||||
splitTestName={inrGeoBannerSplitTestName}
|
||||
/>
|
||||
) : (
|
||||
<GroupsAndEnterpriseBanner />
|
||||
)}
|
||||
|
|
|
@ -765,7 +765,11 @@
|
|||
"increased_compile_timeout": "Increased compile timeout",
|
||||
"indvidual_plans": "Individual Plans",
|
||||
"info": "Info",
|
||||
"inr_discount_modal_info": "Get document history, track changes, additional collaborators, and more at Purchasing Power Parity prices.",
|
||||
"inr_discount_modal_title": "70% off all Overleaf premium plans for users in India",
|
||||
"inr_discount_offer": "Good news! You can now use Rupees ₹ to pay for an Overleaf subscription, giving you a <0>70% discount</0> on our premium features.",
|
||||
"inr_discount_offer_green_banner": "__flag__ <0>Big news!</0> You qualify for a <0>70% discount</0> on our premium plans because you’re in India. <br />Get additional collaborators, document history, track changes, and more.",
|
||||
"inr_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> Because you’re in India, you can get any Overleaf premium plan at a <b>70% discount</b>.",
|
||||
"insert_figure": "Insert figure",
|
||||
"insert_from_another_project": "Insert from another project",
|
||||
"insert_from_project_files": "Insert from project files",
|
||||
|
@ -976,6 +980,7 @@
|
|||
"max_collab_per_project_info": "The number of people you can invite to work on each project. They just need to have an Overleaf account. They can be different people in each project.",
|
||||
"maximum_files_uploaded_together": "Maximum __max__ files uploaded together",
|
||||
"may": "May",
|
||||
"maybe_later": "Maybe later",
|
||||
"members_management": "Members management",
|
||||
"mendeley": "Mendeley",
|
||||
"mendeley_groups_loading_error": "There was an error loading groups from Mendeley",
|
||||
|
|
BIN
services/web/public/img/subscriptions/inr-discount-modal.png
Normal file
BIN
services/web/public/img/subscriptions/inr-discount-modal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Loading…
Reference in a new issue