From c636089939c1bd36a6bc56b0242c3b9fede73405 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Mon, 12 Aug 2024 09:01:58 -0500 Subject: [PATCH] Merge pull request #19822 from overleaf/jel-plans-notifications [web] Show geo banners on light touch redesign of plans page GitOrigin-RevId: 08ee6827cc16ec9326e5dbb1a165602be9cfe86d --- .../Subscription/SubscriptionController.js | 68 ++- .../interstitial-payment-light-design.pug | 6 +- .../subscriptions/plans-light-design.pug | 20 +- .../SubscriptionControllerTests.js | 474 ++++++++++++++++++ 4 files changed, 542 insertions(+), 26 deletions(-) diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index 596dc971f9..b2f6867fa7 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -53,6 +53,15 @@ function _getGroupPlanModalDefaults(req, currency) { } } +function _plansBanners({ geoPricingLATAMTestVariant, countryCode }) { + const showLATAMBanner = + geoPricingLATAMTestVariant === 'latam' && + ['MX', 'CO', 'CL', 'PE'].includes(countryCode) + const showInrGeoBanner = countryCode === 'IN' + const showBrlGeoBanner = countryCode === 'BR' + return { showLATAMBanner, showInrGeoBanner, showBrlGeoBanner } +} + async function plansPage(req, res) { const websiteRedesignPlansAssignment = await SplitTestHandler.promises.getAssignment( @@ -95,9 +104,12 @@ async function plansPage(req, res) { plansPageViewSegmentation ) - const showLATAMBanner = - geoPricingLATAMTestVariant === 'latam' && - ['MX', 'CO', 'CL', 'PE'].includes(countryCode) + const { showLATAMBanner, showInrGeoBanner, showBrlGeoBanner } = _plansBanners( + { + geoPricingLATAMTestVariant, + countryCode, + } + ) const localCcyAssignment = await SplitTestHandler.promises.getAssignment( req, @@ -129,8 +141,8 @@ async function plansPage(req, res) { language, formatCurrency ), - showInrGeoBanner: countryCode === 'IN', - showBrlGeoBanner: countryCode === 'BR', + showInrGeoBanner, + showBrlGeoBanner, showLATAMBanner, latamCountryBannerDetails, }) @@ -144,8 +156,12 @@ async function plansPageLightDesign(req, res) { if (!splitTestActive && req.query.preview !== 'true') { return res.redirect(302, '/user/subscription/plans') } - - const { currency } = await _getRecommendedCurrency(req, res) + const { + currency, + countryCode, + geoPricingLATAMTestVariant, + recommendedCurrency, + } = await _getRecommendedCurrency(req, res) const language = req.i18n.language || 'en' const currentView = 'annual' @@ -153,7 +169,27 @@ async function plansPageLightDesign(req, res) { const groupPlanModalDefaults = _getGroupPlanModalDefaults(req, currency) const formatCurrency = SubscriptionHelper.formatCurrencyDefault - // TODO: add page view analytics? + const { showLATAMBanner, showInrGeoBanner, showBrlGeoBanner } = _plansBanners( + { + geoPricingLATAMTestVariant, + countryCode, + } + ) + + const latamCountryBannerDetails = await getLatamCountryBannerDetails(req, res) + + const plansPageViewSegmentation = { + currency: recommendedCurrency, + countryCode, + 'geo-pricing-latam-v2': geoPricingLATAMTestVariant, + } + + AnalyticsManager.recordEventForSession( + req.session, + 'plans-page-view', + plansPageViewSegmentation + ) + res.render('subscriptions/plans-light-design', { title: 'plans_and_pricing', currentView, @@ -174,6 +210,10 @@ async function plansPageLightDesign(req, res) { language, formatCurrency ), + showLATAMBanner, + showInrGeoBanner, + showBrlGeoBanner, + latamCountryBannerDetails, }) } @@ -332,9 +372,11 @@ async function interstitialPaymentPage(req, res) { paywallPlansPageViewSegmentation ) - const showLATAMBanner = - geoPricingLATAMTestVariant === 'latam' && - ['MX', 'CO', 'CL', 'PE'].includes(countryCode) + const { showLATAMBanner, showInrGeoBanner, showBrlGeoBanner } = + _plansBanners({ + geoPricingLATAMTestVariant, + countryCode, + }) const localCcyAssignment = await SplitTestHandler.promises.getAssignment( req, @@ -355,8 +397,8 @@ async function interstitialPaymentPage(req, res) { ? formatCurrencyLocalized : SubscriptionHelper.formatCurrencyDefault, showCurrencyAndPaymentMethods: localCcyAssignment.variant === 'enabled', - showInrGeoBanner: countryCode === 'IN', - showBrlGeoBanner: countryCode === 'BR', + showInrGeoBanner, + showBrlGeoBanner, showLATAMBanner, latamCountryBannerDetails, skipLinkTarget: req.session?.postCheckoutRedirect || '/project', diff --git a/services/web/app/views/subscriptions/interstitial-payment-light-design.pug b/services/web/app/views/subscriptions/interstitial-payment-light-design.pug index 74e4fab949..0c0883230f 100644 --- a/services/web/app/views/subscriptions/interstitial-payment-light-design.pug +++ b/services/web/app/views/subscriptions/interstitial-payment-light-design.pug @@ -21,13 +21,13 @@ block content .plans-page.plans-page-interstitial .container if showInrGeoBanner - div.notification.notification-type-success.text-center + .mb-5.notification.notification-type-success.text-center div.notification-content !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})} if showBrlGeoBanner - div.notification.notification-type-success.text-center + .mb-5.notification.notification-type-success.text-center div.notification-content !{translate("brl_discount_offer_plans_page_banner", {flag: '🇧🇷'})} if showLATAMBanner - div.notification.notification-type-success.text-center + .mb-5.notification.notification-type-success.text-center div.notification-content !{translate("latam_discount_offer_plans_page_banner", {flag: latamCountryBannerDetails.latamCountryFlag, country: latamCountryBannerDetails.country, currency: latamCountryBannerDetails.currency, discount: latamCountryBannerDetails.discount })} .row diff --git a/services/web/app/views/subscriptions/plans-light-design.pug b/services/web/app/views/subscriptions/plans-light-design.pug index bb7bf13a18..e3b1935299 100644 --- a/services/web/app/views/subscriptions/plans-light-design.pug +++ b/services/web/app/views/subscriptions/plans-light-design.pug @@ -16,19 +16,19 @@ block content main.website-redesign#main-content .plans-page .container - //- if showInrGeoBanner - //- div.notification.notification-type-success.text-centered - //- div.notification-content !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})} - //- 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, currency: latamCountryBannerDetails.currency, discount: latamCountryBannerDetails.discount })} + if showInrGeoBanner + .mb-5.notification.notification-type-success.text-center + div.notification-content !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})} + if showBrlGeoBanner + .mb-5.notification.notification-type-success.text-center + div.notification-content !{translate("brl_discount_offer_plans_page_banner", {flag: '🇧🇷'})} + if showLATAMBanner + .mb-5.notification.notification-type-success.text-center + div.notification-content !{translate("latam_discount_offer_plans_page_banner", {flag: latamCountryBannerDetails.latamCountryFlag, country: latamCountryBannerDetails.country, currency: latamCountryBannerDetails.currency, discount: latamCountryBannerDetails.discount })} .row .col-md-12 - h1.text-centered + h1.text-center +eyebrow(translate('plans_and_pricing_lowercase')) | #{translate('choose_your_plan')} diff --git a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js index 6bb124b0d3..16bad0136f 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js @@ -132,6 +132,7 @@ describe('SubscriptionController', function () { this.SplitTestV2Hander = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), + isSplitTestActive: sinon.stub().resolves(true), }, } this.SubscriptionHelper = { @@ -281,6 +282,342 @@ describe('SubscriptionController', function () { }) this.SubscriptionController.plansPage(this.req, this.res) }) + it('should return false for US Users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showInrGeoBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + }) + + describe('showBrlGeoBanner data', function () { + it('should return true for Brazilian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showBrlGeoBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'BR', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + it('should return false for US users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showBrlGeoBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + }) + + describe('showLATAMBanner', function () { + describe('latam variant', function () { + beforeEach(function () { + this.SplitTestV2Hander.promises.getAssignment.resolves({ + variant: 'latam', + }) + }) + it('should return true for Mexican users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'MX', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + it('should return true for Colombian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'CO', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + it('should return true for Chilean users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'CL', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + it('should return true for Peruvian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'PE', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + it('should return true for US users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showLATAMBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + }) + describe('default variant', function () { + beforeEach(function () { + this.SplitTestV2Hander.promises.getAssignment.resolves({ + variant: 'default', + }) + }) + it('should return false', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans') + opts.showLATAMBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'MX', + }) + this.SubscriptionController.plansPage(this.req, this.res) + }) + }) + }) + }) + + describe('plansPage light touch redesign', function () { + beforeEach(function () { + this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534' + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + currencyCode: this.stubbedCurrencyCode, + }) + }) + + describe('ip override', function () { + beforeEach(function () { + this.req.ip = '1.2.3.4' + this.req.query = { ip: '5.6.7.8' } + this.GeoIpLookup.promises.getCurrencyCode.withArgs('1.2.3.4').resolves({ + currencyCode: 'GBP', + }) + this.GeoIpLookup.promises.getCurrencyCode.withArgs('5.6.7.8').resolves({ + currencyCode: 'USD', + }) + }) + it('should ignore override for non admin', function (done) { + this.res.render = (page, opts) => { + opts.recommendedCurrency.should.equal('GBP') + done() + } + this.AuthorizationManager.promises.isUserSiteAdmin.resolves(false) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + + it('should accept override for admin', function (done) { + this.res.render = (page, opts) => { + opts.recommendedCurrency.should.equal('USD') + done() + } + this.AuthorizationManager.promises.isUserSiteAdmin.resolves(true) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + }) + + describe('groupPlanModal data', function () { + it('should pass local currency if valid', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.groupPlanModalDefaults.currency.should.equal('GBP') + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + currencyCode: 'GBP', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + + it('should fallback to USD when valid', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.groupPlanModalDefaults.currency.should.equal('USD') + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + currencyCode: 'FOO', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + + it('should pass valid options for group plan modal and discard invalid', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.groupPlanModalDefaults.size.should.equal('42') + opts.groupPlanModalDefaults.plan_code.should.equal('collaborator') + opts.groupPlanModalDefaults.currency.should.equal('GBP') + opts.groupPlanModalDefaults.usage.should.equal('foo') + done() + } + this.GeoIpLookup.isValidCurrencyParam.returns(false) + this.req.query = { + number: '42', + currency: 'ABC', + plan: 'does-not-exist', + usage: 'foo', + } + this.SubscriptionController.plansPageLightDesign(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-light-design') + opts.showInrGeoBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'IN', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + it('should return false for US Users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showInrGeoBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + }) + + describe('showBrlGeoBanner data', function () { + it('should return true for Brazilian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showBrlGeoBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'BR', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + it('should return false for US users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showBrlGeoBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + }) + + describe('showLATAMBanner', function () { + describe('latam variant', function () { + beforeEach(function () { + this.SplitTestV2Hander.promises.getAssignment.resolves({ + variant: 'latam', + }) + }) + it('should return true for Mexican users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'MX', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + it('should return true for Colombian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'CO', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + it('should return true for Chilean users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'CL', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + it('should return true for Peruvian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'PE', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + it('should return true for US users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showLATAMBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + }) + describe('default variant', function () { + beforeEach(function () { + this.SplitTestV2Hander.promises.getAssignment.resolves({ + variant: 'default', + }) + }) + it('should return false', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/plans-light-design') + opts.showLATAMBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'MX', + }) + this.SubscriptionController.plansPageLightDesign(this.req, this.res) + }) + }) }) }) @@ -328,6 +665,143 @@ describe('SubscriptionController', function () { }) this.SubscriptionController.interstitialPaymentPage(this.req, this.res) }) + it('should be false for US users', function (done) { + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showInrGeoBanner.should.equal(false) + done() + } + this.SubscriptionController.interstitialPaymentPage(this.req, this.res) + }) + }) + + describe('showBrlGeoBanner data', function () { + it('should return true for Brazilian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showBrlGeoBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'BR', + }) + this.SubscriptionController.interstitialPaymentPage(this.req, this.res) + }) + it('should return false for US users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showBrlGeoBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.SubscriptionController.interstitialPaymentPage(this.req, this.res) + }) + }) + + describe('showLATAMBanner', function () { + describe('latam variant', function () { + beforeEach(function () { + this.SplitTestV2Hander.promises.getAssignment.resolves({ + variant: 'latam', + }) + }) + it('should return true for Mexican users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'MX', + }) + this.SubscriptionController.interstitialPaymentPage( + this.req, + this.res + ) + }) + it('should return true for Colombian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'CO', + }) + this.SubscriptionController.interstitialPaymentPage( + this.req, + this.res + ) + }) + it('should return true for Chilean users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'CL', + }) + this.SubscriptionController.interstitialPaymentPage( + this.req, + this.res + ) + }) + it('should return true for Peruvian users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showLATAMBanner.should.equal(true) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'PE', + }) + this.SubscriptionController.interstitialPaymentPage( + this.req, + this.res + ) + }) + it('should return true for US users', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showLATAMBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'US', + }) + this.SubscriptionController.interstitialPaymentPage( + this.req, + this.res + ) + }) + }) + describe('default variant', function () { + beforeEach(function () { + this.SplitTestV2Hander.promises.getAssignment.resolves({ + variant: 'default', + }) + }) + it('should return false', function (done) { + this.res.render = (page, opts) => { + page.should.equal('subscriptions/interstitial-payment') + opts.showLATAMBanner.should.equal(false) + done() + } + this.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'MX', + }) + this.SubscriptionController.interstitialPaymentPage( + this.req, + this.res + ) + }) + }) }) })