Selaa lähdekoodia

Merge pull request #17230 from overleaf/mf-tear-down-inr-2

[web] Tear down INR split test

GitOrigin-RevId: 90c6ab4666d9281e804b279d697d6c14ffd682d2
M Fahru 2 vuotta sitten
vanhempi
sitoutus
dc9b0aa8a5

+ 2 - 18
services/web/app/src/Features/Project/ProjectListController.js

@@ -363,28 +363,14 @@ async function projectListPage(req, res, next) {
     'writefull-integration'
   )
 
-  let showInrGeoBanner, inrGeoBannerSplitTestName
-  let inrGeoBannerVariant = 'default'
+  let showInrGeoBanner = false
   let recommendedCurrency
+
   if (usersBestSubscription?.type === 'free') {
     const { countryCode } = await GeoIpLookup.promises.getCurrencyCode(req.ip)
-    // Split test is kept active, but all users geolocated in India can
-    // now use the INR currency (See #13507)
-    const { variant: inrGeoPricingVariant } =
-      await SplitTestHandler.promises.getAssignment(req, res, 'geo-pricing-inr')
 
     if (countryCode === 'IN') {
-      inrGeoBannerSplitTestName =
-        inrGeoPricingVariant === 'inr'
-          ? 'geo-banners-inr-2'
-          : 'geo-banners-inr-1'
-      const geoBannerAssignment = await SplitTestHandler.promises.getAssignment(
-        req,
-        res,
-        inrGeoBannerSplitTestName
-      )
       showInrGeoBanner = true
-      inrGeoBannerVariant = geoBannerAssignment.variant
     }
   }
 
@@ -437,8 +423,6 @@ async function projectListPage(req, res, next) {
     showWritefullPromoBanner,
     recommendedCurrency,
     showInrGeoBanner,
-    inrGeoBannerVariant,
-    inrGeoBannerSplitTestName,
     projectDashboardReact: true, // used in navbar
     groupSsoSetupSuccess,
     groupSubscriptionsPendingEnrollment:

+ 10 - 62
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -38,8 +38,10 @@ async function plansPage(req, res) {
   if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
     currency = queryCurrency
   }
-  const { recommendedCurrency, countryCode, geoPricingINRTestVariant } =
-    await _getRecommendedCurrency(req, res)
+  const { recommendedCurrency, countryCode } = await _getRecommendedCurrency(
+    req,
+    res
+  )
   if (recommendedCurrency && currency == null) {
     currency = recommendedCurrency
   }
@@ -65,24 +67,6 @@ async function plansPage(req, res) {
     usage: getDefault('usage', 'usage', 'enterprise'),
   }
 
-  let showInrGeoBanner, inrGeoBannerSplitTestName
-  let inrGeoBannerVariant = 'default'
-  if (countryCode === 'IN') {
-    inrGeoBannerSplitTestName =
-      geoPricingINRTestVariant === 'inr'
-        ? 'geo-banners-inr-2'
-        : 'geo-banners-inr-1'
-    const geoBannerAssignment = await SplitTestHandler.promises.getAssignment(
-      req,
-      res,
-      inrGeoBannerSplitTestName
-    )
-    inrGeoBannerVariant = geoBannerAssignment.variant
-    if (inrGeoBannerVariant !== 'default') {
-      showInrGeoBanner = true
-    }
-  }
-
   // annual plans with the free trial option split test - nudge variant
   const annualTrialsAssignment = await SplitTestHandler.promises.getAssignment(
     req,
@@ -93,13 +77,8 @@ async function plansPage(req, res) {
   const plansPageViewSegmentation = {
     currency: recommendedCurrency,
     countryCode,
-    'geo-pricing-inr-group': geoPricingINRTestVariant,
-    'geo-pricing-inr-page': currency === 'INR' ? 'inr' : 'default',
     'annual-trials': annualTrialsAssignment.variant,
   }
-  if (inrGeoBannerSplitTestName) {
-    plansPageViewSegmentation[inrGeoBannerSplitTestName] = inrGeoBannerVariant
-  }
 
   AnalyticsManager.recordEventForSession(
     req.session,
@@ -122,7 +101,7 @@ async function plansPage(req, res) {
     groupPlanModalDefaults,
     initialLocalizedGroupPrice:
       SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
-    showInrGeoBanner,
+    showInrGeoBanner: countryCode === 'IN',
     annualTrialsAssignment: annualTrialsAssignment?.variant,
   })
 }
@@ -233,8 +212,10 @@ async function userSubscriptionPage(req, res) {
 
 async function interstitialPaymentPage(req, res) {
   const user = SessionManager.getSessionUser(req.session)
-  const { recommendedCurrency, countryCode, geoPricingINRTestVariant } =
-    await _getRecommendedCurrency(req, res)
+  const { recommendedCurrency, countryCode } = await _getRecommendedCurrency(
+    req,
+    res
+  )
 
   const hasSubscription =
     await LimitationsManager.promises.userHasV1OrV2Subscription(user)
@@ -244,24 +225,6 @@ async function interstitialPaymentPage(req, res) {
   if (hasSubscription) {
     res.redirect('/user/subscription?hasSubscription=true')
   } else {
-    let showInrGeoBanner, inrGeoBannerSplitTestName
-    let inrGeoBannerVariant = 'default'
-    if (countryCode === 'IN') {
-      inrGeoBannerSplitTestName =
-        geoPricingINRTestVariant === 'inr'
-          ? 'geo-banners-inr-2'
-          : 'geo-banners-inr-1'
-      const geoBannerAssignment = await SplitTestHandler.promises.getAssignment(
-        req,
-        res,
-        inrGeoBannerSplitTestName
-      )
-      inrGeoBannerVariant = geoBannerAssignment.variant
-      if (inrGeoBannerVariant !== 'default') {
-        showInrGeoBanner = true
-      }
-    }
-
     // annual plans with the free trial option split test - nudge variant
     const annualTrialsAssignment =
       await SplitTestHandler.promises.getAssignment(req, res, 'annual-trials')
@@ -269,14 +232,8 @@ async function interstitialPaymentPage(req, res) {
     const paywallPlansPageViewSegmentation = {
       currency: recommendedCurrency,
       countryCode,
-      'geo-pricing-inr-group': geoPricingINRTestVariant,
-      'geo-pricing-inr-page': recommendedCurrency === 'INR' ? 'inr' : 'default',
       'annual-trials': annualTrialsAssignment.variant,
     }
-    if (inrGeoBannerSplitTestName) {
-      paywallPlansPageViewSegmentation[inrGeoBannerSplitTestName] =
-        inrGeoBannerVariant
-    }
     AnalyticsManager.recordEventForSession(
       req.session,
       'paywall-plans-page-view',
@@ -304,7 +261,7 @@ async function interstitialPaymentPage(req, res) {
       recommendedCurrency,
       interstitialPaymentConfig,
       showSkipLink,
-      showInrGeoBanner,
+      showInrGeoBanner: countryCode === 'IN',
     })
   }
 }
@@ -621,14 +578,6 @@ async function _getRecommendedCurrency(req, res) {
   const currencyLookup = await GeoIpLookup.promises.getCurrencyCode(ip)
   const countryCode = currencyLookup.countryCode
   let recommendedCurrency = currencyLookup.currencyCode
-  // for #12703
-  // Split test is kept active, but all users geolocated in India can
-  // now use the INR currency (See #13507)
-  const assignmentINR = await SplitTestHandler.promises.getAssignment(
-    req,
-    res,
-    'geo-pricing-inr'
-  )
 
   if (['BRL', 'MXN', 'COP', 'CLP', 'PEN'].includes(recommendedCurrency)) {
     recommendedCurrency = GeoIpLookup.DEFAULT_CURRENCY_CODE
@@ -637,7 +586,6 @@ async function _getRecommendedCurrency(req, res) {
   return {
     recommendedCurrency,
     countryCode,
-    geoPricingINRTestVariant: assignmentINR?.variant,
   }
 }
 

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

@@ -29,8 +29,6 @@ block append meta
 	meta(name="ol-showWritefullPromoBanner" data-type="boolean" content=showWritefullPromoBanner)
 	meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
 	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-recommendedCurrency" data-type="string" content=recommendedCurrency)
 	meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
 	meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription)

+ 2 - 1
services/web/app/views/subscriptions/interstitial-payment.pug

@@ -18,7 +18,8 @@ block content
 			.plans
 				.container
 					if showInrGeoBanner
-						div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
+						div.notification.notification-type-success.text-centered
+							div.notification-content !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
 
 					.row
 						.col-md-12

+ 2 - 1
services/web/app/views/subscriptions/interstitial-payment_no_nudge_monthly.pug

@@ -18,7 +18,8 @@ block content
 			.plans
 				.container
 					if showInrGeoBanner
-						div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
+						div.notification.notification-type-success.text-centered
+							div.notification-content !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
 
 					.row
 						.col-md-12

+ 2 - 1
services/web/app/views/subscriptions/interstitial-payment_nudge_annual.pug

@@ -18,7 +18,8 @@ block content
 			.plans
 				.container
 					if showInrGeoBanner
-						div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
+						div.notification.notification-type-success.text-centered
+							div.notification-content !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
 
 					.row
 						.col-md-12

+ 2 - 1
services/web/app/views/subscriptions/plans.pug

@@ -16,7 +16,8 @@ block content
 			.plans
 				.container(ng-cloak)
 					if showInrGeoBanner
-						div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
+						div.notification.notification-type-success.text-centered
+							div.notification-content !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
 
 					.row
 						.col-md-12

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

@@ -578,8 +578,6 @@
   "increased_compile_timeout": "",
   "inr_discount_modal_info": "",
   "inr_discount_modal_title": "",
-  "inr_discount_offer": "",
-  "inr_discount_offer_green_banner": "",
   "insert": "",
   "insert_column_left": "",
   "insert_column_right": "",

+ 48 - 120
services/web/frontend/js/features/project-list/components/notifications/ads/inr-banner.tsx

@@ -1,35 +1,12 @@
 import { useCallback, useEffect, useRef, useState } from 'react'
-import { Trans, useTranslation } from 'react-i18next'
+import { useTranslation } from 'react-i18next'
 import usePersistedState from '../../../../../shared/hooks/use-persisted-state'
-import Notification from '../notification'
 import * as eventTracking from '../../../../../infrastructure/event-tracking'
 import { Modal, Button } from 'react-bootstrap'
 import AccessibleModal from '../../../../../shared/components/accessible-modal'
-import getMeta from '@/utils/meta'
 
-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) {
+export default function INRBanner() {
   const { t } = useTranslation()
-  const newNotificationStyle = getMeta(
-    'ol-newNotificationStyle',
-    false
-  ) as boolean
   const [dismissedUntil, setDismissedUntil] = usePersistedState<
     Date | undefined
   >(`has_dismissed_inr_banner_until`)
@@ -44,38 +21,41 @@ export default function INRBanner({ variant, splitTestName }: INRBannerProps) {
     }
     if (!viewEventSent.current) {
       eventTracking.sendMB('promo-prompt', {
-        location: variant === 'modal' ? 'dashboard-modal' : 'dashboard-banner',
-        name: splitTestName,
-        content: contentLookup[variant],
+        location: 'dashboard-modal',
+        name: 'geo-pricing',
+        content: 'modal',
+        country: 'IN',
       })
       viewEventSent.current = true
     }
-  }, [dismissedUntil, splitTestName, variant])
+  }, [dismissedUntil])
 
   const handleClick = useCallback(() => {
     eventTracking.sendMB('promo-click', {
-      location: variant === 'modal' ? 'dashboard-modal' : 'dashboard-banner',
-      name: splitTestName,
-      content: contentLookup[variant],
+      location: 'dashboard-modal',
+      name: 'geo-pricing',
+      content: 'modal',
       type: 'click',
+      country: 'IN',
     })
 
     setShowModal(false)
 
     window.open('/user/subscription/plans')
-  }, [splitTestName, variant])
+  }, [])
 
   const bannerDismissed = useCallback(() => {
     eventTracking.sendMB('promo-dismiss', {
-      location: variant === 'modal' ? 'dashboard-modal' : 'dashboard-banner',
-      name: splitTestName,
-      content: contentLookup[variant],
+      location: 'dashboard-modal',
+      name: 'geo-pricing',
+      content: 'modal',
       type: 'click',
+      country: 'IN',
     })
     const until = new Date()
     until.setDate(until.getDate() + 30) // 30 days
     setDismissedUntil(until)
-  }, [setDismissedUntil, splitTestName, variant])
+  }, [setDismissedUntil])
 
   const handleHide = useCallback(() => {
     setShowModal(false)
@@ -84,99 +64,47 @@ export default function INRBanner({ variant, splitTestName }: INRBannerProps) {
 
   const handleMaybeLater = useCallback(() => {
     eventTracking.sendMB('promo-click', {
-      location: variant === 'modal' ? 'dashboard-modal' : 'dashboard-banner',
-      name: splitTestName,
-      content: contentLookup[variant],
+      location: 'dashboard-modal',
+      name: 'geo-pricing',
+      content: 'modal',
       type: 'pause',
+      country: 'IN',
     })
     setShowModal(false)
     const until = new Date()
     until.setDate(until.getDate() + 1) // 1 day
     setDismissedUntil(until)
-  }, [setDismissedUntil, splitTestName, variant])
+  }, [setDismissedUntil])
 
   if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
     return null
   }
 
-  if (variant === 'default') {
-    return (
-      <Notification
-        bsStyle="info"
-        onDismiss={bannerDismissed}
-        body={
-          <Trans
-            i18nKey="inr_discount_offer"
-            components={[<b />]} // eslint-disable-line react/jsx-key
-          />
-        }
-        action={
-          <Button
-            bsStyle={newNotificationStyle ? null : 'info'}
-            bsSize="sm"
-            className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
-            onClick={handleClick}
-          >
-            {t('get_discounted_plan')}
-          </Button>
-        }
-      />
-    )
-  } else if (variant === 'green-banner') {
-    return (
-      <Notification
-        bsStyle="success"
-        onDismiss={bannerDismissed}
-        body={
-          <Trans
-            i18nKey="inr_discount_offer_green_banner"
-            components={[<b />, <br />]} // eslint-disable-line react/jsx-key
-            values={{ flag: '🇮🇳' }}
-            shouldUnescape
-            tOptions={{ interpolation: { escapeValue: true } }}
+  return (
+    <AccessibleModal show={showModal} onHide={handleHide} backdrop="static">
+      <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%',
+            }}
           />
-        }
-        action={
-          <Button
-            bsStyle={newNotificationStyle ? null : 'success'}
-            bsSize="sm"
-            className={newNotificationStyle ? 'btn-secondary' : 'pull-right'}
-            onClick={handleClick}
-          >
-            {t('get_discounted_plan')} ₹
-          </Button>
-        }
-      />
-    )
-  } 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>{t('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
-  }
+        </p>
+        <p>{t('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>
+  )
 }

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

@@ -44,11 +44,6 @@ function UserNotifications() {
   )
 
   const showInrGeoBanner = getMeta('ol-showInrGeoBanner', false)
-  const inrGeoBannerVariant = getMeta('ol-inrGeoBannerVariant', 'default')
-  const inrGeoBannerSplitTestName = getMeta(
-    'ol-inrGeoBannerSplitTestName',
-    'unassigned'
-  )
   const writefullIntegrationSplitTestEnabled = isSplitTestEnabled(
     'writefull-integration'
   )
@@ -105,15 +100,8 @@ function UserNotifications() {
         <Institution />
         <ConfirmEmail />
         <ReconfirmationInfo />
-        {!showInrGeoBanner && !showWritefull && !dismissedWritefull && (
-          <GroupsAndEnterpriseBanner />
-        )}
-        {showInrGeoBanner && (
-          <INRBanner
-            variant={inrGeoBannerVariant}
-            splitTestName={inrGeoBannerSplitTestName}
-          />
-        )}
+        {!showWritefull && !dismissedWritefull && <GroupsAndEnterpriseBanner />}
+        {showInrGeoBanner && <INRBanner />}
         {writefullIntegrationSplitTestEnabled || user?.writefull?.enabled ? (
           <WritefullPremiumPromoBanner
             show={showWritefull}

+ 10 - 0
services/web/frontend/stories/project-list/inr-banner.stories.tsx

@@ -0,0 +1,10 @@
+import INRBanner from '@/features/project-list/components/notifications/ads/inr-banner'
+
+export const Default = () => {
+  return <INRBanner />
+}
+
+export default {
+  title: 'Project List / INR Banner',
+  component: INRBanner,
+}

+ 0 - 1
services/web/locales/da.json

@@ -701,7 +701,6 @@
   "increased_compile_timeout": "Forlænget kompileringstidsgrænse",
   "indvidual_plans": "Individuelle abonnementer",
   "info": "Info",
-  "inr_discount_offer": "Gode nyheder! Du kan nu bruge rupier ₹ til at betale for et Overleaf abonnement, hvilket giver dig en <0>70% rabat</0> på vores Premium-funktioner.",
   "insert_figure": "Indsæt figur",
   "insert_from_another_project": "Indsæt fra andet projekt",
   "insert_from_project_files": "Indsæt fra projektfiler",

+ 0 - 1
services/web/locales/de.json

@@ -719,7 +719,6 @@
   "increased_compile_timeout": "Zeitlimit beim Kompilieren erhöhen",
   "indvidual_plans": "Einzelnutzer-Abonnements",
   "info": "Info",
-  "inr_discount_offer": "Gute Nachrichten! Du kannst jetzt für dein Overleaf-Abonnement in Rupees ₹ bezahlen und so einen <0>70% Rabatt</0> auf unsere Premiumfunktionen erhalten.",
   "insert_figure": "Abbildung einfügen",
   "insert_from_another_project": "Von einem anderen Projekt einfügen",
   "insert_from_project_files": "Von Projektdateien einfügen",

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

@@ -845,8 +845,6 @@
   "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. <1/>Get additional collaborators, document history, track changes, and more.",
   "inr_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> We’ve applied a <b>70% discount</b> discount to premium plans for our users in India. Check out the new lower prices below.",
   "insert": "Insert",
   "insert_column_left": "Insert column left",

+ 0 - 2
services/web/locales/zh-CN.json

@@ -855,8 +855,6 @@
   "info": "信息",
   "inr_discount_modal_info": "以平价获取文档历史记录、跟踪更改、更多协作者等功能。",
   "inr_discount_modal_title": "面向印度用户的所有 Overleaf 高级计划七折优惠",
-  "inr_discount_offer": "好消息! 您现在可以使用卢比 ₹ 来支付 Overleaf 订阅费用,从而在使用我们的高级功能时享受<0>70% 的折扣</0>。",
-  "inr_discount_offer_green_banner": "__flag__ <0>好消息!</0>由于您在印度,因此您有资格享受我们的高级计划的<0>70% 折扣</0>。 &lt;1/&gt;获取其他协作者、文档历史记录、跟踪更改等。",
   "inr_discount_offer_plans_page_banner": "__flag__ <b>好消息!</b>我们已为印度用户的高级计划提供<b>70% 折扣</b>折扣。 查看下面的最新低价。",
   "insert": "插入",
   "insert_column_left": "在左边插入列",

+ 84 - 0
services/web/test/frontend/features/project-list/components/inr-banner.test.tsx

@@ -0,0 +1,84 @@
+import INRBanner from '@/features/project-list/components/notifications/ads/inr-banner'
+import customLocalStorage from '@/infrastructure/local-storage'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { expect } from 'chai'
+
+describe('<INRBanner />', function () {
+  beforeEach(function () {
+    customLocalStorage.clear()
+  })
+
+  it('renders correctly', async function () {
+    render(<INRBanner />)
+    await screen.findByRole('dialog')
+
+    await screen.findByText(
+      '70% off all Overleaf premium plans for users in India'
+    )
+
+    await screen.findByText(
+      'Get document history, track changes, additional collaborators, and more at Purchasing Power Parity prices.'
+    )
+
+    await screen.findByRole('button', { name: 'Maybe later' })
+
+    await screen.findByRole('button', { name: 'Get discounted plan' })
+  })
+
+  it('dismisses the modal when the "Maybe later" button is clicked', async function () {
+    render(<INRBanner />)
+    await screen.findByRole('dialog')
+
+    fireEvent.click(screen.getByRole('button', { name: 'Maybe later' }))
+
+    expect(screen.queryByRole('dialog')).to.be.null
+
+    const dismissedUntil = customLocalStorage.getItem(
+      'has_dismissed_inr_banner_until'
+    )
+
+    expect(dismissedUntil).to.not.be.null
+
+    const nowPlus2Days = new Date()
+    nowPlus2Days.setDate(nowPlus2Days.getDate() + 2)
+
+    // check if dismissal date is around 1 days after the dismissal via "Maybe later" button
+    expect(new Date(dismissedUntil)).to.be.greaterThan(new Date())
+    expect(new Date(dismissedUntil)).to.be.lessThan(nowPlus2Days)
+  })
+
+  it('dismisses the modal when close button is clicked', async function () {
+    render(<INRBanner />)
+    await screen.findByRole('dialog')
+
+    fireEvent.click(screen.getByRole('button', { name: /close/i }))
+
+    expect(screen.queryByRole('dialog')).to.be.null
+
+    const dismissedUntil = customLocalStorage.getItem(
+      'has_dismissed_inr_banner_until'
+    )
+
+    expect(dismissedUntil).to.not.be.null
+
+    const nowPlus29Days = new Date()
+    nowPlus29Days.setDate(nowPlus29Days.getDate() + 29)
+
+    const nowPlus31Days = new Date()
+    nowPlus31Days.setDate(nowPlus31Days.getDate() + 31)
+
+    // check if dismissal date is around 30 days after the dismissal via close button
+    expect(new Date(dismissedUntil)).to.be.greaterThan(nowPlus29Days)
+    expect(new Date(dismissedUntil)).to.be.lessThan(nowPlus31Days)
+  })
+
+  it('hides the modal when user visits while current date is less than local storage date', function () {
+    const until = new Date()
+    until.setDate(until.getDate() + 30) // 30 days
+    customLocalStorage.setItem('has_dismissed_inr_banner_until', until)
+
+    render(<INRBanner />)
+
+    expect(screen.queryByRole('dialog')).to.be.null
+  })
+})

+ 41 - 0
services/web/test/unit/src/Project/ProjectListControllerTests.js

@@ -126,6 +126,14 @@ describe('ProjectListController', function () {
         getUserSubscription: sinon.stub().resolves({}),
       },
     }
+    this.GeoIpLookup = {
+      promises: {
+        getCurrencyCode: sinon.stub().resolves({
+          countryCode: 'US',
+          currencyCode: 'USD',
+        }),
+      },
+    }
 
     this.ProjectListController = SandboxedModule.require(MODULE_PATH, {
       requires: {
@@ -155,6 +163,7 @@ describe('ProjectListController', function () {
           this.UserPrimaryEmailCheckHandler,
         '../Notifications/NotificationsBuilder': this.NotificationBuilder,
         '../Subscription/SubscriptionLocator': this.SubscriptionLocator,
+        '../../infrastructure/GeoIpLookup': this.GeoIpLookup,
       },
     })
 
@@ -310,6 +319,38 @@ describe('ProjectListController', function () {
       this.ProjectListController.projectListPage(this.req, this.res)
     })
 
+    it('should show INR Banner for Indian users with free account', function (done) {
+      // usersBestSubscription is only available when saas feature is present
+      this.Features.hasFeature.withArgs('saas').returns(true)
+      this.SubscriptionViewModelBuilder.promises.getBestSubscription.resolves({
+        type: 'free',
+      })
+      this.GeoIpLookup.promises.getCurrencyCode.resolves({
+        countryCode: 'IN',
+      })
+      this.res.render = (pageName, opts) => {
+        expect(opts.showInrGeoBanner).to.be.true
+        done()
+      }
+      this.ProjectListController.projectListPage(this.req, this.res)
+    })
+
+    it('should not show INR Banner for Indian users with premium account', function (done) {
+      // usersBestSubscription is only available when saas feature is present
+      this.Features.hasFeature.withArgs('saas').returns(true)
+      this.SubscriptionViewModelBuilder.promises.getBestSubscription.resolves({
+        type: 'individual',
+      })
+      this.GeoIpLookup.promises.getCurrencyCode.resolves({
+        countryCode: 'IN',
+      })
+      this.res.render = (pageName, opts) => {
+        expect(opts.showInrGeoBanner).to.be.false
+        done()
+      }
+      this.ProjectListController.projectListPage(this.req, this.res)
+    })
+
     describe('With Institution SSO feature', function () {
       beforeEach(function (done) {
         this.institutionEmail = 'test@overleaf.com'

+ 28 - 0
services/web/test/unit/src/Subscription/SubscriptionControllerTests.js

@@ -268,6 +268,20 @@ describe('SubscriptionController', function () {
         this.SubscriptionController.plansPage(this.req, this.res)
       })
     })
+
+    describe('showInrGeoBanner data', function () {
+      it('should return true for Indian Users', function (done) {
+        this.res.render = (page, opts) => {
+          page.should.equal('subscriptions/plans')
+          opts.showInrGeoBanner.should.equal(true)
+          done()
+        }
+        this.GeoIpLookup.promises.getCurrencyCode.resolves({
+          countryCode: 'IN',
+        })
+        this.SubscriptionController.plansPage(this.req, this.res)
+      })
+    })
   })
 
   describe('interstitialPaymentPage', function () {
@@ -301,6 +315,20 @@ describe('SubscriptionController', function () {
         this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
       })
     })
+
+    describe('showInrGeoBanner data', function () {
+      it('should return true for Indian users', function (done) {
+        this.res.render = (page, opts) => {
+          page.should.equal('subscriptions/interstitial-payment')
+          opts.showInrGeoBanner.should.equal(true)
+          done()
+        }
+        this.GeoIpLookup.promises.getCurrencyCode.resolves({
+          countryCode: 'IN',
+        })
+        this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
+      })
+    })
   })
 
   describe('successfulSubscription', function () {