From b6c38ef5d0d6cc528cb6cff1c12283ebc1a73c1e Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Thu, 26 Feb 2026 16:24:53 +0100 Subject: [PATCH] [web] Show student discount pre checkout (#31820) * Compute student discount from prices * Add presentational discount in the checkout page * Put student discount row behind feature flag * Update code and tests to clarify that `currency` is always defined * Introduce `usePlanPriceItems` to normalize the list * Simplify `usePlanPriceItems` Co-authored-by: Olzhas Askar * Remove student discount percent * Update Standard Monthly/Annual names in the checkout page * Simplify `getRecommendedCurrency` mock * Fix testid: price-summary-plan * Add test on stripe-price-summary * Add `Math.abs` on accessibility discounted info (!) --------- Co-authored-by: Olzhas Askar GitOrigin-RevId: f297eab4b6abd6a84842054667a3734cb33866fe --- .../src/Features/Project/ProjectListController.mjs | 3 ++- .../Subscription/SubscriptionController.mjs | 7 +++++++ .../web/app/src/infrastructure/GeoIpLookup.mjs | 14 ++++++++++++-- services/web/frontend/extracted-translations.json | 1 + services/web/locales/en.json | 3 ++- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index a3a677e710..ef6c668244 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/services/web/app/src/Features/Project/ProjectListController.mjs @@ -496,7 +496,8 @@ async function projectListPage(req, res, next) { showInrGeoBanner = true } - showLATAMBanner = ['MX', 'CO', 'CL', 'PE'].includes(countryCode) + showLATAMBanner = + !!countryCode && ['MX', 'CO', 'CL', 'PE'].includes(countryCode) // LATAM Banner needs to know which currency to display if (showLATAMBanner) { recommendedCurrency = currencyCode diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.mjs b/services/web/app/src/Features/Subscription/SubscriptionController.mjs index 820023d8a8..ca0e54dd58 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionController.mjs @@ -48,6 +48,10 @@ const { const SUBSCRIPTION_PAUSED_REDIRECT_PATH = '/user/subscription?redirect-reason=subscription-paused' +/** + * @typedef {import('../../../../types/subscription/currency').CurrencyCode} CurrencyCode + */ + /** * Check if a Stripe subscription is currently paused * @param {Object} subscription - The subscription object @@ -972,6 +976,9 @@ async function refreshUserFeatures(req, res) { res.sendStatus(200) } +/** + * @returns {Promise<{currency: CurrencyCode, recommendedCurrency: CurrencyCode, countryCode: string|undefined}>} + */ async function getRecommendedCurrency(req, res) { const userId = SessionManager.getLoggedInUserId(req.session) let ip = req.ip diff --git a/services/web/app/src/infrastructure/GeoIpLookup.mjs b/services/web/app/src/infrastructure/GeoIpLookup.mjs index 4c7ff12466..21b09d7cce 100644 --- a/services/web/app/src/infrastructure/GeoIpLookup.mjs +++ b/services/web/app/src/infrastructure/GeoIpLookup.mjs @@ -1,9 +1,16 @@ +// @ts-check + import settings from '@overleaf/settings' import logger from '@overleaf/logger' import { fetchJson } from '@overleaf/fetch-utils' -const DEFAULT_CURRENCY_CODE = 'USD' +/** + * @typedef {import('../../../types/subscription/currency').CurrencyCode} CurrencyCode + */ +const DEFAULT_CURRENCY_CODE = /** @type {const} */ 'USD' + +/** @type {Record} */ const currencyMappings = { GB: 'GBP', US: 'USD', @@ -84,6 +91,9 @@ async function getDetails(ip, callback) { return await fetchJson(url, { signal: AbortSignal.timeout(1_000) }) } +/** + * @returns {Promise<{currencyCode: CurrencyCode, countryCode: string|undefined}>} + */ async function getCurrencyCode(ip) { let ipDetails try { @@ -93,7 +103,7 @@ async function getCurrencyCode(ip) { { err, ip }, `problem getting currencyCode for ip, defaulting to ${DEFAULT_CURRENCY_CODE}` ) - return { currencyCode: DEFAULT_CURRENCY_CODE } + return { currencyCode: DEFAULT_CURRENCY_CODE, countryCode: undefined } } const countryCode = ipDetails && ipDetails.country_code diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index c73ad484ac..87c39dc689 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -1812,6 +1812,7 @@ "strongly_disagree": "", "student": "", "student_disclaimer": "", + "student_discount": "", "subject": "", "subject_area": "", "subject_to_additional_vat": "", diff --git a/services/web/locales/en.json b/services/web/locales/en.json index fe187242b4..c784a13048 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -191,7 +191,7 @@ "apply_educational_discount": "Apply educational discount", "apply_educational_discount_description": "40% discount for groups using __appName__ for teaching", "apply_educational_discount_description_with_group_discount": "Get a total of 40% off for groups using __appName__ for teaching", - "apply_educational_discount_individual": "Apply 50% student discount", + "apply_student_discount": "Apply student discount", "apply_suggestion": "Apply suggestion", "april": "April", "archive": "Archive", @@ -2305,6 +2305,7 @@ "strongly_disagree": "Strongly disagree", "student": "Student", "student_disclaimer": "The educational discount applies to all students at secondary and postsecondary institutions (schools and universities). We may contact you to confirm that you’re eligible for the discount.", + "student_discount": "Student discount", "student_verification_required": "Student verification required", "students": "Students", "subject": "Subject",