mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
[web] INR Geo pricing test (#12976)
* Implement INR geopricing test * Show again the INR banner after 30 days * Update INRBanner to direct users to the plans page and add tracking * Remove local testing hack in GeoIpLookup * Update formatter for subscription dashboard * Flip assignment to assign all users and add event segmentations * Fix linting * Review suggestions - factorised recommended currency helper function GitOrigin-RevId: b1616520f8c7ead689a840720057297e67d3f574
This commit is contained in:
parent
acfeafd276
commit
caeceba28c
10 changed files with 177 additions and 37 deletions
|
@ -21,6 +21,7 @@ const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandl
|
||||||
const UserController = require('../User/UserController')
|
const UserController = require('../User/UserController')
|
||||||
const LimitationsManager = require('../Subscription/LimitationsManager')
|
const LimitationsManager = require('../Subscription/LimitationsManager')
|
||||||
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
|
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
|
||||||
|
const GeoIpLookup = require('../../infrastructure/GeoIpLookup')
|
||||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||||
|
|
||||||
/** @typedef {import("./types").GetProjectsRequest} GetProjectsRequest */
|
/** @typedef {import("./types").GetProjectsRequest} GetProjectsRequest */
|
||||||
|
@ -337,6 +338,27 @@ async function projectListPage(req, res, next) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let showINRBanner = false
|
||||||
|
if (usersBestSubscription?.type === 'free') {
|
||||||
|
try {
|
||||||
|
const inrGeoPricingAssignment =
|
||||||
|
await SplitTestHandler.promises.getAssignment(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
'geo-pricing-inr'
|
||||||
|
)
|
||||||
|
const geoDetails = await GeoIpLookup.promises.getDetails(req.ip)
|
||||||
|
showINRBanner =
|
||||||
|
inrGeoPricingAssignment.variant === 'inr' &&
|
||||||
|
geoDetails?.country_code === 'IN'
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ err: error },
|
||||||
|
'Failed to get INR geo pricing lookup or assignment'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.render('project/list-react', {
|
res.render('project/list-react', {
|
||||||
title: 'your_projects',
|
title: 'your_projects',
|
||||||
usersBestSubscription,
|
usersBestSubscription,
|
||||||
|
@ -354,6 +376,7 @@ async function projectListPage(req, res, next) {
|
||||||
showGroupsAndEnterpriseBanner,
|
showGroupsAndEnterpriseBanner,
|
||||||
groupsAndEnterpriseBannerVariant,
|
groupsAndEnterpriseBannerVariant,
|
||||||
showWritefullPromoBanner,
|
showWritefullPromoBanner,
|
||||||
|
showINRBanner,
|
||||||
projectDashboardReact: true, // used in navbar
|
projectDashboardReact: true, // used in navbar
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,18 +34,15 @@ const validGroupPlanModalOptions = {
|
||||||
async function plansPage(req, res) {
|
async function plansPage(req, res) {
|
||||||
const plans = SubscriptionViewModelBuilder.buildPlansList()
|
const plans = SubscriptionViewModelBuilder.buildPlansList()
|
||||||
|
|
||||||
let recommendedCurrency
|
let currency = null
|
||||||
if (req.query.currency) {
|
const queryCurrency = req.query.currency?.toUpperCase()
|
||||||
const queryCurrency = req.query.currency.toUpperCase()
|
if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
|
||||||
if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
|
currency = queryCurrency
|
||||||
recommendedCurrency = queryCurrency
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!recommendedCurrency) {
|
const { recommendedCurrency, countryCode, geoPricingTestVariant } =
|
||||||
const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(
|
await _getRecommendedCurrency(req, res)
|
||||||
(req.query ? req.query.ip : undefined) || req.ip
|
if (recommendedCurrency && currency == null) {
|
||||||
)
|
currency = recommendedCurrency
|
||||||
recommendedCurrency = currencyLookup.currencyCode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefault(param, category, defaultValue) {
|
function getDefault(param, category, defaultValue) {
|
||||||
|
@ -59,8 +56,8 @@ async function plansPage(req, res) {
|
||||||
const currentView = 'annual'
|
const currentView = 'annual'
|
||||||
|
|
||||||
let defaultGroupPlanModalCurrency = 'USD'
|
let defaultGroupPlanModalCurrency = 'USD'
|
||||||
if (validGroupPlanModalOptions.currency.includes(recommendedCurrency)) {
|
if (validGroupPlanModalOptions.currency.includes(currency)) {
|
||||||
defaultGroupPlanModalCurrency = recommendedCurrency
|
defaultGroupPlanModalCurrency = currency
|
||||||
}
|
}
|
||||||
const groupPlanModalDefaults = {
|
const groupPlanModalDefaults = {
|
||||||
plan_code: getDefault('plan', 'plan_code', 'collaborator'),
|
plan_code: getDefault('plan', 'plan_code', 'collaborator'),
|
||||||
|
@ -70,7 +67,10 @@ async function plansPage(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalyticsManager.recordEventForSession(req.session, 'plans-page-view', {
|
AnalyticsManager.recordEventForSession(req.session, 'plans-page-view', {
|
||||||
currency: recommendedCurrency,
|
currency,
|
||||||
|
countryCode,
|
||||||
|
'geo-pricing-inr-group': geoPricingTestVariant,
|
||||||
|
'geo-pricing-inr-page': currency === 'INR' ? 'inr' : 'default',
|
||||||
})
|
})
|
||||||
|
|
||||||
res.render('subscriptions/plans-marketing-v2', {
|
res.render('subscriptions/plans-marketing-v2', {
|
||||||
|
@ -80,16 +80,14 @@ async function plansPage(req, res) {
|
||||||
itm_content: req.query?.itm_content,
|
itm_content: req.query?.itm_content,
|
||||||
itm_referrer: req.query?.itm_referrer,
|
itm_referrer: req.query?.itm_referrer,
|
||||||
itm_campaign: 'plans',
|
itm_campaign: 'plans',
|
||||||
recommendedCurrency,
|
recommendedCurrency: currency,
|
||||||
planFeatures,
|
planFeatures,
|
||||||
plansV2Config,
|
plansV2Config,
|
||||||
groupPlans: GroupPlansData,
|
groupPlans: GroupPlansData,
|
||||||
groupPlanModalOptions,
|
groupPlanModalOptions,
|
||||||
groupPlanModalDefaults,
|
groupPlanModalDefaults,
|
||||||
initialLocalizedGroupPrice:
|
initialLocalizedGroupPrice:
|
||||||
SubscriptionHelper.generateInitialLocalizedGroupPrice(
|
SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
|
||||||
recommendedCurrency
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,10 +145,8 @@ async function _paymentReactPage(req, res) {
|
||||||
currency = queryCurrency
|
currency = queryCurrency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { currencyCode: recommendedCurrency, countryCode } =
|
const { recommendedCurrency, countryCode } =
|
||||||
await GeoIpLookup.promises.getCurrencyCode(
|
await _getRecommendedCurrency(req, res)
|
||||||
(req.query ? req.query.ip : undefined) || req.ip
|
|
||||||
)
|
|
||||||
if (recommendedCurrency && currency == null) {
|
if (recommendedCurrency && currency == null) {
|
||||||
currency = recommendedCurrency
|
currency = recommendedCurrency
|
||||||
}
|
}
|
||||||
|
@ -205,9 +201,7 @@ async function _paymentAngularPage(req, res) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { currencyCode: recommendedCurrency, countryCode } =
|
const { currencyCode: recommendedCurrency, countryCode } =
|
||||||
await GeoIpLookup.promises.getCurrencyCode(
|
await GeoIpLookup.promises.getCurrencyCode(req.query?.ip || req.ip)
|
||||||
(req.query ? req.query.ip : undefined) || req.ip
|
|
||||||
)
|
|
||||||
if (recommendedCurrency && currency == null) {
|
if (recommendedCurrency && currency == null) {
|
||||||
currency = recommendedCurrency
|
currency = recommendedCurrency
|
||||||
}
|
}
|
||||||
|
@ -378,10 +372,8 @@ async function _userSubscriptionAngularPage(req, res) {
|
||||||
|
|
||||||
async function interstitialPaymentPage(req, res) {
|
async function interstitialPaymentPage(req, res) {
|
||||||
const user = SessionManager.getSessionUser(req.session)
|
const user = SessionManager.getSessionUser(req.session)
|
||||||
const { currencyCode: recommendedCurrency } =
|
const { recommendedCurrency, countryCode, geoPricingTestVariant } =
|
||||||
await GeoIpLookup.promises.getCurrencyCode(
|
await _getRecommendedCurrency(req, res)
|
||||||
(req.query ? req.query.ip : undefined) || req.ip
|
|
||||||
)
|
|
||||||
|
|
||||||
const hasSubscription =
|
const hasSubscription =
|
||||||
await LimitationsManager.promises.userHasV1OrV2Subscription(user)
|
await LimitationsManager.promises.userHasV1OrV2Subscription(user)
|
||||||
|
@ -391,9 +383,21 @@ async function interstitialPaymentPage(req, res) {
|
||||||
if (hasSubscription) {
|
if (hasSubscription) {
|
||||||
res.redirect('/user/subscription?hasSubscription=true')
|
res.redirect('/user/subscription?hasSubscription=true')
|
||||||
} else {
|
} else {
|
||||||
|
AnalyticsManager.recordEventForSession(
|
||||||
|
req.session,
|
||||||
|
'paywall-plans-page-view',
|
||||||
|
{
|
||||||
|
currency: recommendedCurrency,
|
||||||
|
countryCode,
|
||||||
|
'geo-pricing-inr-group': geoPricingTestVariant,
|
||||||
|
'geo-pricing-inr-page':
|
||||||
|
recommendedCurrency === 'INR' ? 'inr' : 'default',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
res.render('subscriptions/interstitial-payment', {
|
res.render('subscriptions/interstitial-payment', {
|
||||||
title: 'subscribe',
|
title: 'subscribe',
|
||||||
itm_content: req.query && req.query.itm_content,
|
itm_content: req.query?.itm_content,
|
||||||
itm_campaign: req.query?.itm_campaign,
|
itm_campaign: req.query?.itm_campaign,
|
||||||
itm_referrer: req.query?.itm_referrer,
|
itm_referrer: req.query?.itm_referrer,
|
||||||
recommendedCurrency,
|
recommendedCurrency,
|
||||||
|
@ -808,6 +812,38 @@ async function redirectToHostedPage(req, res) {
|
||||||
res.redirect(url)
|
res.redirect(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _getRecommendedCurrency(req, res) {
|
||||||
|
const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(
|
||||||
|
req.query?.ip || req.ip
|
||||||
|
)
|
||||||
|
const countryCode = currencyLookup.countryCode
|
||||||
|
let recommendedCurrency = currencyLookup.currencyCode
|
||||||
|
let assignment
|
||||||
|
// for #12703
|
||||||
|
try {
|
||||||
|
assignment = await SplitTestHandler.promises.getAssignment(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
'geo-pricing-inr'
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ err: error },
|
||||||
|
'Failed to get assignment for geo-pricing-inr 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' && assignment.variant !== 'inr') {
|
||||||
|
recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
recommendedCurrency,
|
||||||
|
countryCode,
|
||||||
|
geoPricingTestVariant: assignment.variant,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plansPage: expressify(plansPage),
|
plansPage: expressify(plansPage),
|
||||||
paymentPage: expressify(paymentPage),
|
paymentPage: expressify(paymentPage),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const dateformat = require('dateformat')
|
const dateformat = require('dateformat')
|
||||||
|
|
||||||
const currenySymbols = {
|
const currencySymbols = {
|
||||||
EUR: '€',
|
EUR: '€',
|
||||||
USD: '$',
|
USD: '$',
|
||||||
GBP: '£',
|
GBP: '£',
|
||||||
|
@ -12,6 +12,7 @@ const currenySymbols = {
|
||||||
NZD: '$',
|
NZD: '$',
|
||||||
CHF: 'Fr',
|
CHF: 'Fr',
|
||||||
SGD: '$',
|
SGD: '$',
|
||||||
|
INR: '₹',
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -31,7 +32,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
const cents = string.slice(-2)
|
const cents = string.slice(-2)
|
||||||
const dollars = string.slice(0, -2)
|
const dollars = string.slice(0, -2)
|
||||||
const symbol = currenySymbols[currency]
|
const symbol = currencySymbols[currency]
|
||||||
return `${symbol}${dollars}.${cents}`
|
return `${symbol}${dollars}.${cents}`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ const logger = require('@overleaf/logger')
|
||||||
const { URL } = require('url')
|
const { URL } = require('url')
|
||||||
const { promisify, promisifyMultiResult } = require('../util/promises')
|
const { promisify, promisifyMultiResult } = require('../util/promises')
|
||||||
|
|
||||||
|
const DEFAULT_CURRENCY_CODE = 'USD'
|
||||||
|
|
||||||
const currencyMappings = {
|
const currencyMappings = {
|
||||||
GB: 'GBP',
|
GB: 'GBP',
|
||||||
US: 'USD',
|
US: 'USD',
|
||||||
|
@ -16,9 +18,13 @@ const currencyMappings = {
|
||||||
CA: 'CAD',
|
CA: 'CAD',
|
||||||
SE: 'SEK',
|
SE: 'SEK',
|
||||||
SG: 'SGD',
|
SG: 'SGD',
|
||||||
|
IN: 'INR',
|
||||||
}
|
}
|
||||||
|
|
||||||
const validCurrencyParams = Object.values(currencyMappings).concat(['EUR'])
|
const validCurrencyParams = Object.values(currencyMappings).concat([
|
||||||
|
'EUR',
|
||||||
|
'INR',
|
||||||
|
])
|
||||||
|
|
||||||
// Countries which would likely prefer Euro's
|
// Countries which would likely prefer Euro's
|
||||||
const EuroCountries = [
|
const EuroCountries = [
|
||||||
|
@ -82,15 +88,15 @@ function getCurrencyCode(ip, callback) {
|
||||||
if (err || !ipDetails) {
|
if (err || !ipDetails) {
|
||||||
logger.err(
|
logger.err(
|
||||||
{ err, ip },
|
{ err, ip },
|
||||||
'problem getting currencyCode for ip, defaulting to USD'
|
`problem getting currencyCode for ip, defaulting to ${DEFAULT_CURRENCY_CODE}`
|
||||||
)
|
)
|
||||||
return callback(null, 'USD')
|
return callback(null, DEFAULT_CURRENCY_CODE)
|
||||||
}
|
}
|
||||||
const countryCode =
|
const countryCode =
|
||||||
ipDetails && ipDetails.country_code
|
ipDetails && ipDetails.country_code
|
||||||
? ipDetails.country_code.toUpperCase()
|
? ipDetails.country_code.toUpperCase()
|
||||||
: undefined
|
: undefined
|
||||||
const currencyCode = currencyMappings[countryCode] || 'USD'
|
const currencyCode = currencyMappings[countryCode] || DEFAULT_CURRENCY_CODE
|
||||||
logger.debug({ ip, currencyCode, ipDetails }, 'got currencyCode for ip')
|
logger.debug({ ip, currencyCode, ipDetails }, 'got currencyCode for ip')
|
||||||
callback(err, currencyCode, countryCode)
|
callback(err, currencyCode, countryCode)
|
||||||
})
|
})
|
||||||
|
@ -107,4 +113,5 @@ module.exports = {
|
||||||
'countryCode',
|
'countryCode',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
DEFAULT_CURRENCY_CODE,
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ block append meta
|
||||||
meta(name="ol-showGroupsAndEnterpriseBanner" data-type="boolean" content=showGroupsAndEnterpriseBanner)
|
meta(name="ol-showGroupsAndEnterpriseBanner" data-type="boolean" content=showGroupsAndEnterpriseBanner)
|
||||||
meta(name="ol-showWritefullPromoBanner" data-type="boolean" content=showWritefullPromoBanner)
|
meta(name="ol-showWritefullPromoBanner" data-type="boolean" content=showWritefullPromoBanner)
|
||||||
meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
|
meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
|
||||||
|
meta(name="ol-showINRBanner" data-type="boolean" content=showINRBanner)
|
||||||
|
|
||||||
block content
|
block content
|
||||||
main.content.content-alt.project-list-react#project-list-root
|
main.content.content-alt.project-list-react#project-list-root
|
||||||
|
|
|
@ -313,6 +313,7 @@
|
||||||
"generic_linked_file_compile_error": "",
|
"generic_linked_file_compile_error": "",
|
||||||
"generic_something_went_wrong": "",
|
"generic_something_went_wrong": "",
|
||||||
"get_collaborative_benefits": "",
|
"get_collaborative_benefits": "",
|
||||||
|
"get_discounted_plan": "",
|
||||||
"get_most_subscription_by_checking_features": "",
|
"get_most_subscription_by_checking_features": "",
|
||||||
"get_most_subscription_by_checking_premium_features": "",
|
"get_most_subscription_by_checking_premium_features": "",
|
||||||
"git": "",
|
"git": "",
|
||||||
|
@ -414,6 +415,7 @@
|
||||||
"in_order_to_match_institutional_metadata_2": "",
|
"in_order_to_match_institutional_metadata_2": "",
|
||||||
"in_order_to_match_institutional_metadata_associated": "",
|
"in_order_to_match_institutional_metadata_associated": "",
|
||||||
"increased_compile_timeout": "",
|
"increased_compile_timeout": "",
|
||||||
|
"inr_discount_offer": "",
|
||||||
"institution": "",
|
"institution": "",
|
||||||
"institution_account": "",
|
"institution_account": "",
|
||||||
"institution_acct_successfully_linked_2": "",
|
"institution_acct_successfully_linked_2": "",
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useCallback, useEffect } 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'
|
||||||
|
|
||||||
|
export default function INRBanner() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [dismissedAt, setDismissedAt] = usePersistedState<Date | undefined>(
|
||||||
|
`has_dismissed_inr_banner`
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
eventTracking.sendMB('paywall-prompt', {
|
||||||
|
'paywall-type': 'inr-banner',
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
eventTracking.sendMB('paywall-click', { 'paywall-type': 'inr-banner' })
|
||||||
|
|
||||||
|
window.open('/user/subscription/plans')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (dismissedAt) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Notification bsStyle="info" onDismiss={() => setDismissedAt(new Date())}>
|
||||||
|
<Notification.Body>
|
||||||
|
<Trans
|
||||||
|
i18nKey="inr_discount_offer"
|
||||||
|
components={[<b />]} // eslint-disable-line react/jsx-key
|
||||||
|
/>
|
||||||
|
</Notification.Body>
|
||||||
|
<Notification.Action>
|
||||||
|
<Button
|
||||||
|
bsStyle="info"
|
||||||
|
bsSize="sm"
|
||||||
|
className="pull-right"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{t('get_discounted_plan')}
|
||||||
|
</Button>
|
||||||
|
</Notification.Action>
|
||||||
|
</Notification>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,8 +4,12 @@ import ConfirmEmail from './groups/confirm-email'
|
||||||
import ReconfirmationInfo from './groups/affiliation/reconfirmation-info'
|
import ReconfirmationInfo from './groups/affiliation/reconfirmation-info'
|
||||||
import GroupsAndEnterpriseBanner from './groups-and-enterprise-banner'
|
import GroupsAndEnterpriseBanner from './groups-and-enterprise-banner'
|
||||||
import WritefullPromoBanner from './writefull-promo-banner'
|
import WritefullPromoBanner from './writefull-promo-banner'
|
||||||
|
import INRBanner from './ads/inr-banner'
|
||||||
|
import getMeta from '../../../../utils/meta'
|
||||||
|
|
||||||
function UserNotifications() {
|
function UserNotifications() {
|
||||||
|
const showIRNBanner = getMeta('ol-showINRBanner')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-notifications">
|
<div className="user-notifications">
|
||||||
<ul className="list-unstyled">
|
<ul className="list-unstyled">
|
||||||
|
@ -13,7 +17,7 @@ function UserNotifications() {
|
||||||
<Institution />
|
<Institution />
|
||||||
<ConfirmEmail />
|
<ConfirmEmail />
|
||||||
<ReconfirmationInfo />
|
<ReconfirmationInfo />
|
||||||
<GroupsAndEnterpriseBanner />
|
{showIRNBanner ? <INRBanner /> : <GroupsAndEnterpriseBanner />}
|
||||||
<WritefullPromoBanner />
|
<WritefullPromoBanner />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,7 @@ export const currencies = <const>{
|
||||||
NZD: '$',
|
NZD: '$',
|
||||||
CHF: 'Fr',
|
CHF: 'Fr',
|
||||||
SGD: '$',
|
SGD: '$',
|
||||||
|
INR: '₹',
|
||||||
}
|
}
|
||||||
|
|
||||||
type Currency = typeof currencies
|
type Currency = typeof currencies
|
||||||
|
|
|
@ -566,6 +566,7 @@
|
||||||
"generic_linked_file_compile_error": "This project’s output files are not available because it failed to compile. Please open the project to see the compilation error details.",
|
"generic_linked_file_compile_error": "This project’s output files are not available because it failed to compile. Please open the project to see the compilation error details.",
|
||||||
"generic_something_went_wrong": "Sorry, something went wrong",
|
"generic_something_went_wrong": "Sorry, something went wrong",
|
||||||
"get_collaborative_benefits": "Get the collaborative benefits from __appName__, even if you prefer to work offline",
|
"get_collaborative_benefits": "Get the collaborative benefits from __appName__, even if you prefer to work offline",
|
||||||
|
"get_discounted_plan": "Get discounted plan",
|
||||||
"get_in_touch_having_problems": "<a href=\"__link__\">Get in touch with support</a> if you’re having problems",
|
"get_in_touch_having_problems": "<a href=\"__link__\">Get in touch with support</a> if you’re having problems",
|
||||||
"get_involved": "Get involved",
|
"get_involved": "Get involved",
|
||||||
"get_most_subscription_by_checking_features": "Get the most out of your __appName__ subscription by checking out <0>__appName__’s features</0>.",
|
"get_most_subscription_by_checking_features": "Get the most out of your __appName__ subscription by checking out <0>__appName__’s features</0>.",
|
||||||
|
@ -710,6 +711,7 @@
|
||||||
"increased_compile_timeout": "Increased compile timeout",
|
"increased_compile_timeout": "Increased compile timeout",
|
||||||
"indvidual_plans": "Individual Plans",
|
"indvidual_plans": "Individual Plans",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
|
"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.",
|
||||||
"institution": "Institution",
|
"institution": "Institution",
|
||||||
"institution_account": "Institution Account",
|
"institution_account": "Institution Account",
|
||||||
"institution_account_tried_to_add_affiliated_with_another_institution": "This email is <b>already associated</b> with your account but affiliated with another institution.",
|
"institution_account_tried_to_add_affiliated_with_another_institution": "This email is <b>already associated</b> with your account but affiliated with another institution.",
|
||||||
|
|
Loading…
Reference in a new issue