Parcourir la source

Merge pull request #17559 from overleaf/ds-latam-v2

LATAM Geo Pricing - Introducing split test in LATAM countries Round 2

GitOrigin-RevId: 76ab880262f126f8db7f8e577154d78b5e9abf7a
Davinder Singh il y a 2 ans
Parent
commit
998d55d159

+ 19 - 1
services/web/app/src/Features/Project/ProjectListController.js

@@ -359,15 +359,32 @@ async function projectListPage(req, res, next) {
 
   let showInrGeoBanner = false
   let showBrlGeoBanner = false
+  let showLATAMBanner = false
   let recommendedCurrency
 
   if (usersBestSubscription?.type === 'free') {
-    const { countryCode } = await GeoIpLookup.promises.getCurrencyCode(req.ip)
+    const latamGeoPricingAssignment =
+      await SplitTestHandler.promises.getAssignment(
+        req,
+        res,
+        'geo-pricing-latam-v2'
+      )
+
+    const { countryCode, currencyCode } =
+      await GeoIpLookup.promises.getCurrencyCode(req.ip)
 
     if (countryCode === 'IN') {
       showInrGeoBanner = true
     }
     showBrlGeoBanner = countryCode === 'BR'
+
+    showLATAMBanner =
+      latamGeoPricingAssignment.variant === 'latam' &&
+      ['MX', 'CO', 'CL', 'PE'].includes(countryCode)
+    // LATAM Banner needs to know which currency to display
+    if (showLATAMBanner) {
+      recommendedCurrency = currencyCode
+    }
   }
 
   let hasIndividualRecurlySubscription = false
@@ -426,6 +443,7 @@ async function projectListPage(req, res, next) {
     showGroupsAndEnterpriseBanner,
     groupsAndEnterpriseBannerVariant,
     showWritefullPromoBanner,
+    showLATAMBanner,
     recommendedCurrency,
     showInrGeoBanner,
     showBrlGeoBanner,

+ 72 - 9
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -37,10 +37,10 @@ async function plansPage(req, res) {
   if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
     currency = queryCurrency
   }
-  const { recommendedCurrency, countryCode } = await _getRecommendedCurrency(
-    req,
-    res
-  )
+  const { recommendedCurrency, countryCode, geoPricingLATAMTestVariant } =
+    await _getRecommendedCurrency(req, res)
+
+  const latamCountryBannerDetails = await getLatamCountryBannerDetails(req, res)
   if (recommendedCurrency && currency == null) {
     currency = recommendedCurrency
   }
@@ -68,6 +68,7 @@ async function plansPage(req, res) {
   const plansPageViewSegmentation = {
     currency: recommendedCurrency,
     countryCode,
+    'geo-pricing-latam-v2': geoPricingLATAMTestVariant,
   }
 
   AnalyticsManager.recordEventForSession(
@@ -76,6 +77,10 @@ async function plansPage(req, res) {
     plansPageViewSegmentation
   )
 
+  const showLATAMBanner =
+    geoPricingLATAMTestVariant === 'latam' &&
+    ['MX', 'CO', 'CL', 'PE'].includes(countryCode)
+
   res.render('subscriptions/plans', {
     title: 'plans_and_pricing',
     currentView,
@@ -93,6 +98,8 @@ async function plansPage(req, res) {
       SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
     showInrGeoBanner: countryCode === 'IN',
     showBrlGeoBanner: countryCode === 'BR',
+    showLATAMBanner,
+    latamCountryBannerDetails,
   })
 }
 
@@ -202,10 +209,10 @@ async function userSubscriptionPage(req, res) {
 
 async function interstitialPaymentPage(req, res) {
   const user = SessionManager.getSessionUser(req.session)
-  const { recommendedCurrency, countryCode } = await _getRecommendedCurrency(
-    req,
-    res
-  )
+  const { recommendedCurrency, countryCode, geoPricingLATAMTestVariant } =
+    await _getRecommendedCurrency(req, res)
+
+  const latamCountryBannerDetails = await getLatamCountryBannerDetails(req, res)
 
   const hasSubscription =
     await LimitationsManager.promises.userHasV1OrV2Subscription(user)
@@ -217,6 +224,7 @@ async function interstitialPaymentPage(req, res) {
     const paywallPlansPageViewSegmentation = {
       currency: recommendedCurrency,
       countryCode,
+      'geo-pricing-latam-v2': geoPricingLATAMTestVariant,
     }
     AnalyticsManager.recordEventForSession(
       req.session,
@@ -224,6 +232,10 @@ async function interstitialPaymentPage(req, res) {
       paywallPlansPageViewSegmentation
     )
 
+    const showLATAMBanner =
+      geoPricingLATAMTestVariant === 'latam' &&
+      ['MX', 'CO', 'CL', 'PE'].includes(countryCode)
+
     res.render('subscriptions/interstitial-payment', {
       title: 'subscribe',
       itm_content: req.query?.itm_content,
@@ -234,6 +246,8 @@ async function interstitialPaymentPage(req, res) {
       showSkipLink,
       showInrGeoBanner: countryCode === 'IN',
       showBrlGeoBanner: countryCode === 'BR',
+      showLATAMBanner,
+      latamCountryBannerDetails,
     })
   }
 }
@@ -551,16 +565,65 @@ async function _getRecommendedCurrency(req, res) {
   const countryCode = currencyLookup.countryCode
   let recommendedCurrency = currencyLookup.currencyCode
 
-  if (['MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency)) {
+  const assignmentLATAM = await SplitTestHandler.promises.getAssignment(
+    req,
+    res,
+    'geo-pricing-latam-v2'
+  )
+
+  if (
+    ['MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency) &&
+    assignmentLATAM?.variant === 'default'
+  ) {
     recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
   }
 
   return {
     recommendedCurrency,
     countryCode,
+    geoPricingLATAMTestVariant: assignmentLATAM?.variant,
   }
 }
 
+async function getLatamCountryBannerDetails(req, res) {
+  const userId = SessionManager.getLoggedInUserId(req.session)
+  let ip = req.ip
+  if (
+    req.query?.ip &&
+    (await AuthorizationManager.promises.isUserSiteAdmin(userId))
+  ) {
+    ip = req.query.ip
+  }
+  const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip)
+  const countryCode = currencyLookup.countryCode
+  const latamCountryBannerDetails = {}
+
+  switch (countryCode) {
+    case `MX`:
+      latamCountryBannerDetails.latamCountryFlag = '🇲🇽'
+      latamCountryBannerDetails.country = 'Mexico'
+      latamCountryBannerDetails.discount = '25%'
+      break
+    case `CO`:
+      latamCountryBannerDetails.latamCountryFlag = '🇨🇴'
+      latamCountryBannerDetails.country = 'Colombia'
+      latamCountryBannerDetails.discount = '60%'
+      break
+    case `CL`:
+      latamCountryBannerDetails.latamCountryFlag = '🇨🇱'
+      latamCountryBannerDetails.country = 'Chile'
+      latamCountryBannerDetails.discount = '30%'
+      break
+    case `PE`:
+      latamCountryBannerDetails.latamCountryFlag = '🇵🇪'
+      latamCountryBannerDetails.country = 'Peru'
+      latamCountryBannerDetails.discount = '40%'
+      break
+  }
+
+  return latamCountryBannerDetails
+}
+
 module.exports = {
   plansPage: expressify(plansPage),
   userSubscriptionPage: expressify(userSubscriptionPage),

+ 1 - 0
services/web/app/views/project/list-react.pug

@@ -31,6 +31,7 @@ block append meta
 	meta(name="ol-showInrGeoBanner" data-type="boolean" content=showInrGeoBanner)
 	meta(name="ol-showBrlGeoBanner" data-type="boolean" content=showBrlGeoBanner)
 	meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency)
+	meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
 	meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
 	meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)
 	meta(name="ol-newNotificationStyle" data-type="boolean" content=newNotificationStyle)

+ 3 - 0
services/web/app/views/subscriptions/interstitial-payment.pug

@@ -23,6 +23,9 @@ block content
 					if showBrlGeoBanner
 						div.notification.notification-type-success.text-centered
 							div.notification-content !{translate("brl_discount_offer_plans_page_banner", {flag: '🇧🇷'})}
+					if showLATAMBanner
+						div.notification.notification-type-success.text-centered
+							div.notification-content !{translate("latam_discount_offer_plans_page_banner", {flag: latamCountryBannerDetails.latamCountryFlag, country: latamCountryBannerDetails.country, discount: latamCountryBannerDetails.discount })}
 
 					.row
 						.col-md-12

+ 3 - 0
services/web/app/views/subscriptions/plans.pug

@@ -21,6 +21,9 @@ block content
 					if showBrlGeoBanner
 						div.notification.notification-type-success.text-centered
 							div.notification-content !{translate("brl_discount_offer_plans_page_banner", {flag: '🇧🇷'})}
+					if showLATAMBanner
+						div.notification.notification-type-success.text-centered
+							div.notification-content !{translate("latam_discount_offer_plans_page_banner", {flag: latamCountryBannerDetails.latamCountryFlag, country: latamCountryBannerDetails.country, discount: latamCountryBannerDetails.discount })}
 
 					.row
 						.col-md-12

+ 2 - 2
services/web/frontend/extracted-translations.json

@@ -107,8 +107,6 @@
   "binary_history_error": "",
   "blank_project": "",
   "blocked_filename": "",
-  "brl_discount_modal_info": "",
-  "brl_discount_modal_title": "",
   "browser": "",
   "bulk_accept_confirm": "",
   "bulk_reject_confirm": "",
@@ -655,6 +653,8 @@
   "last_resort_trouble_shooting_guide": "",
   "last_updated_date_by_x": "",
   "last_used": "",
+  "latam_discount_modal_info": "",
+  "latam_discount_modal_title": "",
   "latex_places_figures_according_to_a_special_algorithm": "",
   "latex_places_tables_according_to_a_special_algorithm": "",
   "layout": "",

+ 8 - 3
services/web/frontend/js/features/project-list/components/notifications/ads/brl-banner.tsx

@@ -85,19 +85,24 @@ export default function BRLBanner() {
   return (
     <AccessibleModal show={showModal} onHide={handleHide}>
       <Modal.Header closeButton>
-        <Modal.Title>{t('brl_discount_modal_title')}</Modal.Title>
+        <Modal.Title>{t('latam_discount_modal_title')}</Modal.Title>
       </Modal.Header>
       <Modal.Body className="modal-body-share">
         <p>
           <img
-            alt={t('brl_discount_modal_title')}
+            alt={t('latam_discount_modal_title')}
             src="/img/subscriptions/blr-discount-modal.png"
             style={{
               width: '100%',
             }}
           />
         </p>
-        <p>{t('brl_discount_modal_info')}</p>
+        <p>
+          {t('latam_discount_modal_info', {
+            discount: '50%',
+            currencyName: 'Brazilian Reais',
+          })}
+        </p>
       </Modal.Body>
       <Modal.Footer>
         <Button bsStyle="default" onClick={handleMaybeLater}>

+ 157 - 0
services/web/frontend/js/features/project-list/components/notifications/ads/latam-banner.tsx

@@ -0,0 +1,157 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import usePersistedState from '../../../../../shared/hooks/use-persisted-state'
+import * as eventTracking from '../../../../../infrastructure/event-tracking'
+import { Modal, Button } from 'react-bootstrap'
+import AccessibleModal from '../../../../../shared/components/accessible-modal'
+import { useTranslation } from 'react-i18next'
+import getMeta from '@/utils/meta'
+
+const LATAM_CURRENCIES = {
+  MXN: {
+    name: 'Mexican Pesos',
+    countryCode: 'MX',
+    discountCode: '25',
+    imageSource: '/img/subscriptions/mexico-discount-modal.png',
+  },
+  COP: {
+    name: 'Colombian Pesos',
+    countryCode: 'CO',
+    discountCode: '60',
+    imageSource: '/img/subscriptions/colombia-discount-modal.png',
+  },
+  CLP: {
+    name: 'Chilean Pesos',
+    countryCode: 'CL',
+    discountCode: '30',
+    imageSource: '/img/subscriptions/chile-discount-modal.png',
+  },
+  PEN: {
+    name: 'Peruvian Soles',
+    countryCode: 'PE',
+    discountCode: '40',
+    imageSource: '/img/subscriptions/peru-discount-modal.png',
+  },
+}
+
+export default function LATAMBanner() {
+  const { t } = useTranslation()
+  const [dismissedUntil, setDismissedUntil] = usePersistedState<
+    Date | undefined
+  >(`has_dismissed_latam_banner_until`)
+  const viewEventSent = useRef<boolean>(false)
+  const [showModal, setShowModal] = useState(true)
+
+  const currency = getMeta('ol-recommendedCurrency')
+  const {
+    imageSource,
+    name: currencyName,
+    discountCode,
+    countryCode,
+  } = LATAM_CURRENCIES[currency as keyof typeof LATAM_CURRENCIES]
+
+  useEffect(() => {
+    if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
+      return
+    }
+    if (!viewEventSent.current) {
+      eventTracking.sendMB('promo-prompt', {
+        location: 'dashboard-modal',
+        name: 'geo-pricing',
+        page: '/project',
+        content: 'modal',
+        country: countryCode,
+      })
+      viewEventSent.current = true
+    }
+  }, [dismissedUntil, countryCode])
+
+  const handleClick = useCallback(() => {
+    eventTracking.sendMB('promo-click', {
+      location: 'dashboard-modal',
+      name: 'geo-pricing',
+      page: '/project',
+      content: 'modal',
+      country: countryCode,
+      type: 'click',
+    })
+
+    setShowModal(false)
+
+    window.open('/user/subscription/plans')
+  }, [countryCode])
+
+  const bannerDismissed = useCallback(() => {
+    eventTracking.sendMB('promo-dismiss', {
+      location: 'dashboard-modal',
+      name: 'geo-pricing',
+      page: '/project',
+      content: 'modal',
+      country: countryCode,
+    })
+    const until = new Date()
+    until.setDate(until.getDate() + 30) // 30 days
+    setDismissedUntil(until)
+  }, [setDismissedUntil, countryCode])
+
+  const handleHide = useCallback(() => {
+    setShowModal(false)
+    bannerDismissed()
+  }, [bannerDismissed])
+
+  const handleMaybeLater = useCallback(() => {
+    eventTracking.sendMB('promo-click', {
+      location: 'dashboard-modal',
+      name: 'geo-pricing',
+      page: '/project',
+      content: 'modal',
+      country: countryCode,
+      type: 'pause',
+    })
+    setShowModal(false)
+    const until = new Date()
+    until.setDate(until.getDate() + 1) // 1 day
+    setDismissedUntil(until)
+  }, [setDismissedUntil, countryCode])
+
+  if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
+    return null
+  }
+
+  // Safety, but should always be a valid LATAM currency if ol-showLATAMBanner is true
+  if (!(currency in LATAM_CURRENCIES)) {
+    return null
+  }
+
+  return (
+    <AccessibleModal show={showModal} onHide={handleHide} backdrop="static">
+      <Modal.Header closeButton>
+        <Modal.Title>{t('latam_discount_modal_title')}</Modal.Title>
+      </Modal.Header>
+      <Modal.Body className="modal-body-share">
+        <p>
+          <img
+            alt={t('latam_discount_modal_title')}
+            src={imageSource}
+            style={{
+              width: '100%',
+            }}
+          />
+        </p>
+        <p>
+          {t('latam_discount_modal_info', {
+            discount: discountCode,
+            currencyName,
+          })}
+        </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>
+  )
+}

+ 19 - 0
services/web/frontend/js/features/project-list/components/notifications/geo-banners.tsx

@@ -0,0 +1,19 @@
+import getMeta from '@/utils/meta'
+import BRLBanner from './ads/brl-banner'
+import INRBanner from './ads/inr-banner'
+import LATAMBanner from './ads/latam-banner'
+
+function GeoBanners() {
+  const showInrGeoBanner = getMeta('ol-showInrGeoBanner', false)
+  const showBrlGeoBanner = getMeta('ol-showBrlGeoBanner', false)
+  const showLATAMBanner = getMeta('ol-showLATAMBanner', false)
+  return (
+    <>
+      {showBrlGeoBanner && <BRLBanner />}
+      {showLATAMBanner && <LATAMBanner />}
+      {showInrGeoBanner && <INRBanner />}
+    </>
+  )
+}
+
+export default GeoBanners

+ 2 - 6
services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx

@@ -6,13 +6,12 @@ import ReconfirmationInfo from './groups/affiliation/reconfirmation-info'
 import GroupsAndEnterpriseBanner from './groups-and-enterprise-banner'
 import WritefullPremiumPromoBanner from './writefull-premium-promo-banner'
 import GroupSsoSetupSuccess from './groups/group-sso-setup-success'
-import INRBanner from './ads/inr-banner'
 import getMeta from '../../../../utils/meta'
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
 import customLocalStorage from '../../../../infrastructure/local-storage'
 import { sendMB } from '../../../../infrastructure/event-tracking'
 import classNames from 'classnames'
-import BRLBanner from './ads/brl-banner'
+import GeoBanners from './geo-banners'
 
 type Subscription = {
   groupId: string
@@ -36,8 +35,6 @@ function UserNotifications() {
     'ol-groupSubscriptionsPendingEnrollment',
     []
   )
-  const showInrGeoBanner = getMeta('ol-showInrGeoBanner', false)
-  const showBrlGeoBanner = getMeta('ol-showBrlGeoBanner', false)
   const user = getMeta('ol-user')
 
   // Temporary workaround to prevent also showing groups/enterprise banner
@@ -85,8 +82,8 @@ function UserNotifications() {
         <Institution />
         <ConfirmEmail />
         <ReconfirmationInfo />
+        <GeoBanners />
         {!showWritefull && !dismissedWritefull && <GroupsAndEnterpriseBanner />}
-        {showInrGeoBanner && <INRBanner />}
 
         <WritefullPremiumPromoBanner
           show={showWritefull}
@@ -95,7 +92,6 @@ function UserNotifications() {
             setDismissedWritefull(true)
           }}
         />
-        {showBrlGeoBanner && <BRLBanner />}
       </ul>
     </div>
   )

+ 3 - 2
services/web/locales/en.json

@@ -176,8 +176,6 @@
   "bonus_please_recommend_us": "Bonus - Please recommend us",
   "bonus_share_link_text": "Online LaTeX Editor __appName__",
   "bonus_twitter_share_text": "I’m using __appName__, the free online collaborative LaTeX editor - it’s awesome and easy to use!",
-  "brl_discount_modal_info": "Unlock the full potential of Overleaf with a 50% discount on premium subscriptions paid in Brazilian Reais. Get a longer compile timeout, full document history, track changes, additional collaborators, and more.",
-  "brl_discount_modal_title": "Premium subscription discount",
   "brl_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> We’ve applied a 50% discount to premium plans on this page for our users in Brazil. Check out the new lower prices.",
   "browser": "Browser",
   "built_in": "Built-In",
@@ -972,6 +970,9 @@
   "last_updated": "Last Updated",
   "last_updated_date_by_x": "__lastUpdatedDate__ by __person__",
   "last_used": "last used",
+  "latam_discount_modal_info": "Unlock the full potential of Overleaf with a __discount__% discount on premium subscriptions paid in __currencyName__. Get a longer compile timeout, full document history, track changes, additional collaborators, and more.",
+  "latam_discount_modal_title": "Premium subscription discount",
+  "latam_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> We’ve applied a __discount__ discount to premium plans on this page for our users in __country__. Check out the new lower prices.",
   "latex_articles_page_summary": "Papers, presentations, reports and more, written in LaTeX and published by our community. Search or browse below.",
   "latex_articles_page_title": "Articles - Papers, Presentations, Reports and more",
   "latex_editor_info": "Everything you need in a modern LaTeX editor --- spell check, intelligent autocomplete, syntax highlighting, dozens of color themes, vim and emacs bindings, help with LaTeX warnings and error messages, and much more.",

+ 2 - 2
services/web/locales/pt.json

@@ -85,8 +85,6 @@
   "bonus_please_recommend_us": "Bônus - Por favor nos recomende",
   "bonus_share_link_text": "Editor Online de LaTeX __appName__",
   "bonus_twitter_share_text": "Estou usando o __appName__, o editor gratuito online e colaborativo de LaTeX - é muito bom e fácil de usar!",
-  "brl_discount_modal_info": "Obtenha todo o potencial do Overleaf com desconto de 50% em assinaturas premium pagas em reais. Obtenha um tempo limite de compilação mais longo, histórico completo de documentos, controle de alterações, colaboradores adicionais e muito mais.",
-  "brl_discount_modal_title": "Desconto em assinaturas premium",
   "brl_discount_offer_plans_page_banner": "__flag__ Aplicamos um desconto de 50% aos planos premium nesta página para nossos usuários no Brasil. Confira os novos preços mais baixos.",
   "built_in": "Embutido",
   "bulk_accept_confirm": "Vocês tem certeza que deseja aceitar as __nChanges__ alterações selecionadas?",
@@ -327,6 +325,8 @@
   "language": "Idioma",
   "last_modified": "Última Modificação",
   "last_name": "Sobrenome",
+  "latam_discount_modal_info": "Obtenha todo o potencial do Overleaf com desconto de __discount__% em assinaturas premium pagas em __currencyName__. Obtenha um tempo limite de compilação mais longo, histórico completo de documentos, controle de alterações, colaboradores adicionais e muito mais.",
+  "latam_discount_modal_title": "Desconto em assinaturas premium",
   "latex_editor_info": "Tudo que você precisa num editar moderno de LaTeX - correção ortográfica, auto-completar inteligente, realce de sintaxe, diversas cores de temas, atalhos de vim e emacs, ajuda com mensagens de aviso e erros do LaTeX e muito mais.",
   "latex_templates": "Modelos LaTeX",
   "ldap": "LDAP",

BIN
services/web/public/img/subscriptions/chile-discount-modal.png


BIN
services/web/public/img/subscriptions/colombia-discount-modal.png


BIN
services/web/public/img/subscriptions/mexico-discount-modal.png


BIN
services/web/public/img/subscriptions/peru-discount-modal.png