From 82834560b9789b082ddf0eb12c94e43afd7f2514 Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Wed, 20 May 2026 12:14:04 +0200 Subject: [PATCH 1/4] feat: update renewal label --- src/app/i18n/locales/en.json | 2 +- src/views/Checkout/components/CheckoutProductCard.tsx | 2 +- src/views/Checkout/utils/formatPrice.test.ts | 10 +++++++--- src/views/Checkout/utils/formatPrice.ts | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index d628d1d51..408f5dc4c 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -305,7 +305,7 @@ }, "apply": "Apply", "taxes": "Taxes", - "annualBillingTemplate": "Annual plan, billed monthly at {{price}}{{currency}}/month for 12 months" + "annualBillingTemplate": "Annual plan, billed at {{currency}}{{priceNow}} for the first month, then renews at {{currency}}{{price}}/month" }, "confirmCryptoPayment": { "title": "Confirm the payment", diff --git a/src/views/Checkout/components/CheckoutProductCard.tsx b/src/views/Checkout/components/CheckoutProductCard.tsx index 9625ceaf0..34f3d96a6 100644 --- a/src/views/Checkout/components/CheckoutProductCard.tsx +++ b/src/views/Checkout/components/CheckoutProductCard.tsx @@ -256,7 +256,7 @@ export const CheckoutProductCard = ({ {priceData.interval === 'month' && ( -

+

{translate('checkout.productCard.annualBillingTemplate', { priceNow: totalAmountFormatted, price: normalPriceAmount, diff --git a/src/views/Checkout/utils/formatPrice.test.ts b/src/views/Checkout/utils/formatPrice.test.ts index 25bb3c5e5..30be0a80d 100644 --- a/src/views/Checkout/utils/formatPrice.test.ts +++ b/src/views/Checkout/utils/formatPrice.test.ts @@ -21,14 +21,18 @@ describe('Formatting the price to have 2 decimals', () => { }); describe('The price has more than 2 decimals', () => { - it('When the price has more than 2 decimals, then the function returns the price with 2 decimals (truncated with high precision, 10.456 -> 10.45 - 10.001 -> 10 - 1.999 -> 1.99)', () => { - expect(formatPrice(10.456)).toBe('10.45'); + it('When the price has more than 2 decimals, then the function returns the price rounded to 2 decimals (10.456 -> 10.46 - 10.001 -> 10 - 1.999 -> 2)', () => { + expect(formatPrice(10.456)).toBe('10.46'); expect(formatPrice(10.001)).toBe('10'); - expect(formatPrice(1.999)).toBe('1.99'); + expect(formatPrice(1.999)).toBe('2'); }); }); it('Handles edge case where value is nearly integer due to float error', () => { expect(formatPrice(10.0000001)).toBe('10'); }); + + it('Handles floating point precision error (19.99 * 100 = 1998.999... in JS)', () => { + expect(formatPrice(19.99)).toBe('19.99'); + }); }); diff --git a/src/views/Checkout/utils/formatPrice.ts b/src/views/Checkout/utils/formatPrice.ts index 5af8b6f64..3e369047e 100644 --- a/src/views/Checkout/utils/formatPrice.ts +++ b/src/views/Checkout/utils/formatPrice.ts @@ -1,5 +1,5 @@ export const formatPrice = (price: number) => { - const truncated = Math.floor(Number(price.toFixed(8)) * 100) / 100; + const truncated = Math.round(Number(price.toFixed(8)) * 100) / 100; const formatted = truncated.toFixed(2); return formatted.endsWith('.00') ? String(truncated) : formatted; }; From d0ef7a45501d320e913626baf0163de1ad6c8ad3 Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Fri, 22 May 2026 09:03:13 +0200 Subject: [PATCH 2/4] updates for review --- src/app/i18n/locales/de.json | 2 +- src/app/i18n/locales/es.json | 2 +- src/app/i18n/locales/fr.json | 2 +- src/app/i18n/locales/it.json | 2 +- src/app/i18n/locales/ru.json | 2 +- src/app/i18n/locales/tw.json | 2 +- src/app/i18n/locales/zh.json | 2 +- src/views/Checkout/utils/formatPrice.test.ts | 6 +++--- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 433f10201..659a7fa5c 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -257,7 +257,7 @@ }, "apply": "Anwenden", "taxes": "Steuern", - "annualBillingTemplate": "Jahresplan, monatlich abgerechnet mit {{price}}{{currency}}/Monat für 12 Monate" + "annualBillingTemplate": "Jahresplan, abgerechnet mit {{currency}}{{priceNow}} für den ersten Monat, dann Verlängerung zu {{currency}}{{price}}/Monat" }, "confirmCryptoPayment": { "title": "Zahlung bestätigen", diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index c799b026e..40a88069b 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -284,7 +284,7 @@ }, "apply": "Aplicar", "taxes": "Impuestos", - "annualBillingTemplate": "Plan anual, facturado mensualmente a {{price}}{{currency}}/mes durante 12 meses" + "annualBillingTemplate": "Plan anual, facturado a {{currency}}{{priceNow}} el primer mes, luego se renueva a {{currency}}{{price}}/mes" }, "confirmCryptoPayment": { "title": "Confirmar el pago", diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index f6df5ff0d..5b720c095 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -257,7 +257,7 @@ }, "apply": "Appliquer", "taxes": "Taxes", - "annualBillingTemplate": "Plan annuel, facturé mensuellement à {{price}}{{currency}}/mois pendant 12 mois" + "annualBillingTemplate": "Plan annuel, facturé {{currency}}{{priceNow}} pour le premier mois, puis renouvelé à {{currency}}{{price}}/mois" }, "confirmCryptoPayment": { "title": "Confirmer le paiement", diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index a96f726f6..478e2878a 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -348,7 +348,7 @@ }, "apply": "Applica", "taxes": "Tasse", - "annualBillingTemplate": "Piano annuale, fatturato mensilmente a {{price}}{{currency}}/mese per 12 mesi" + "annualBillingTemplate": "Piano annuale, fatturato {{currency}}{{priceNow}} per il primo mese, poi si rinnova a {{currency}}{{price}}/mese" }, "confirmCryptoPayment": { "title": "Conferma il pagamento", diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index 53cbedc49..69282559b 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -257,7 +257,7 @@ }, "apply": "Применить", "taxes": "Налоги", - "annualBillingTemplate": "Годовой план, ежемесячная оплата {{price}}{{currency}}/мес. в течение 12 месяцев" + "annualBillingTemplate": "Годовой план, списание {{currency}}{{priceNow}} за первый месяц, затем продление по {{currency}}{{price}}/мес." }, "confirmCryptoPayment": { "title": "Подтвердить платеж", diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index ec29d0635..4ec006828 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -273,7 +273,7 @@ }, "apply": "應用", "taxes": "稅金", - "annualBillingTemplate": "年度計劃,每月 {{price}}{{currency}}/月,持續 12 個月" + "annualBillingTemplate": "年度計劃,首月收費 {{currency}}{{priceNow}},之後每月 {{currency}}{{price}}/月 自動續訂" }, "confirmCryptoPayment": { "title": "確認付款", diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index a358c3d7c..98d6c5769 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -271,7 +271,7 @@ }, "apply": "应用", "taxes": "税费", - "annualBillingTemplate": "年度计划,每月 {{price}}{{currency}}/月,持续 12 个月" + "annualBillingTemplate": "年度计划,首月收费 {{currency}}{{priceNow}},之后每月 {{currency}}{{price}}/月 自动续订" }, "confirmCryptoPayment": { "title": "确认付款", diff --git a/src/views/Checkout/utils/formatPrice.test.ts b/src/views/Checkout/utils/formatPrice.test.ts index 30be0a80d..bf7c03ffd 100644 --- a/src/views/Checkout/utils/formatPrice.test.ts +++ b/src/views/Checkout/utils/formatPrice.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, test, expect } from 'vitest'; import { formatPrice } from './formatPrice'; describe('Formatting the price to have 2 decimals', () => { @@ -28,11 +28,11 @@ describe('Formatting the price to have 2 decimals', () => { }); }); - it('Handles edge case where value is nearly integer due to float error', () => { + test('When the value is nearly integer due to float error, then it returns without decimals', () => { expect(formatPrice(10.0000001)).toBe('10'); }); - it('Handles floating point precision error (19.99 * 100 = 1998.999... in JS)', () => { + test('When there is a floating point precision error (19.99 * 100 = 1998.999... in JS), then it returns 2 decimals', () => { expect(formatPrice(19.99)).toBe('19.99'); }); }); From 516220a05033557d98089a1e6be96b446765c239 Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Fri, 29 May 2026 09:17:15 +0200 Subject: [PATCH 3/4] update test --- src/app/i18n/locales/en.json | 2 +- src/views/Checkout/utils/formatPrice.test.ts | 22 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index bb21ed734..5dbf649d9 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -258,7 +258,7 @@ "emailMustNotBeEmpty": "Email must not be empty", "privacyGuarantee": "Privacy guarantee: We do not share your information and will contact you only as needed to provide our service." }, - "addressBillingTitle": "Address Billing", + "addressBillingTitle": "Billing Address", "crypto": "Crypto", "addressBilling": { "optional": { diff --git a/src/views/Checkout/utils/formatPrice.test.ts b/src/views/Checkout/utils/formatPrice.test.ts index bf7c03ffd..6e45710fd 100644 --- a/src/views/Checkout/utils/formatPrice.test.ts +++ b/src/views/Checkout/utils/formatPrice.test.ts @@ -6,26 +6,22 @@ describe('Formatting the price to have 2 decimals', () => { expect(formatPrice(10)).toBe('10'); }); - describe('The value has less or exactly 2 decimals', () => { - it('When the user has a price with 1 decimal, the function returns 2 decimals (100.5 -> 100.50)', () => { - expect(formatPrice(100.5)).toBe('100.50'); - }); + it('When the user has a price with 1 decimal, the function returns 2 decimals (100.5 -> 100.50)', () => { + expect(formatPrice(100.5)).toBe('100.50'); + }); - it('When the user has a price with 2 decimals, the function returns 2 decimals (99.99 -> 99.99)', () => { - expect(formatPrice(99.99)).toBe('99.99'); - }); + it('When the user has a price with 2 decimals, the function returns 2 decimals (99.99 -> 99.99)', () => { + expect(formatPrice(99.99)).toBe('99.99'); }); it('When the price is just 1 decimal and it is a 0, then the function returns without .00', () => { expect(formatPrice(20.0)).toBe('20'); }); - describe('The price has more than 2 decimals', () => { - it('When the price has more than 2 decimals, then the function returns the price rounded to 2 decimals (10.456 -> 10.46 - 10.001 -> 10 - 1.999 -> 2)', () => { - expect(formatPrice(10.456)).toBe('10.46'); - expect(formatPrice(10.001)).toBe('10'); - expect(formatPrice(1.999)).toBe('2'); - }); + test('When the price has more than 2 decimals, then the function returns the price rounded to 2 decimals (10.456 -> 10.46 - 10.001 -> 10 - 1.999 -> 2)', () => { + expect(formatPrice(10.456)).toBe('10.46'); + expect(formatPrice(10.001)).toBe('10'); + expect(formatPrice(1.999)).toBe('2'); }); test('When the value is nearly integer due to float error, then it returns without decimals', () => { From 86ef365fb31a7a12427bf86491582115929277b2 Mon Sep 17 00:00:00 2001 From: jaaaaavier Date: Fri, 29 May 2026 16:39:32 +0200 Subject: [PATCH 4/4] Update formatPrice.test.ts --- src/app/i18n/locales/de.json | 5 +++-- src/app/i18n/locales/en.json | 5 +++-- src/app/i18n/locales/es.json | 5 +++-- src/app/i18n/locales/fr.json | 5 +++-- src/app/i18n/locales/it.json | 5 +++-- src/app/i18n/locales/ru.json | 5 +++-- src/app/i18n/locales/tw.json | 5 +++-- src/app/i18n/locales/zh.json | 5 +++-- src/views/Checkout/utils/formatPrice.test.ts | 12 +++++++----- .../Sections/Account/Plans/components/PlanCard.tsx | 2 +- 10 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json index 0fcbf4f1b..4e0e25c7a 100644 --- a/src/app/i18n/locales/de.json +++ b/src/app/i18n/locales/de.json @@ -2008,7 +2008,7 @@ "free": "Erhalten Sie bis zu 10 GB kostenlos", "manageBilling": "Abrechnung verwalten", "freeForever": "Für immer kostenlos", - "billedAnnually": "jährlich abgerechnet", + "billedAnnually": "/Monat jährlich abgerechnet", "billedMonthly": "monatlich abgerechnet", "year": "Jahr", "month": "Monat", @@ -2053,7 +2053,8 @@ "Einladen, teilen & zusammenarbeiten", "Verschlüsseltes VPN", "Antivirus", - "Cleaner (Bereinigungstool)" + "Cleaner (Bereinigungstool)", + "Meet" ] }, "5TB": { diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json index 5dbf649d9..96b5603aa 100644 --- a/src/app/i18n/locales/en.json +++ b/src/app/i18n/locales/en.json @@ -2092,7 +2092,7 @@ "free": "Get up to 10 GB for free", "manageBilling": "Manage billing", "freeForever": "Free forever", - "billedAnnually": "billed annually", + "billedAnnually": "/mo billed annually", "billedMonthly": "billed monthly", "year": "Year", "month": "Month", @@ -2137,7 +2137,8 @@ "Invite, share & collaborate", "Encrypted VPN", "Antivirus", - "Cleaner" + "Cleaner", + "Meet" ] }, "5TB": { diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json index 749fa316c..ee1daef6d 100644 --- a/src/app/i18n/locales/es.json +++ b/src/app/i18n/locales/es.json @@ -2071,7 +2071,7 @@ "year": "Año", "month": "Mes", "lifetime": "Lifetime", - "billedAnnually": "facturado anualmente", + "billedAnnually": "/mes facturado anualmente", "billedMonthly": "facturado mensualmente", "types": { "essential": "Essential", @@ -2113,7 +2113,8 @@ "Invitar, compartir y colaborar", "VPN Cifrada", "Antivirus", - "Cleaner" + "Cleaner", + "Meet" ] }, "5TB": { diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json index 9f3d30317..176365465 100644 --- a/src/app/i18n/locales/fr.json +++ b/src/app/i18n/locales/fr.json @@ -2014,7 +2014,7 @@ "free": "Obtenez jusqu'à 10 Go gratuitement", "manageBilling": "Gérer la facturation", "freeForever": "Gratuit à vie", - "billedAnnually": "facturé annuellement", + "billedAnnually": "/mois facturé annuellement", "billedMonthly": "facturé mensuellement", "year": "Année", "month": "Mois", @@ -2059,7 +2059,8 @@ "Inviter, partager et collaborer", "VPN Chiffré", "Antivirus", - "Nettoyeur" + "Nettoyeur", + "Meet" ] }, "5TB": { diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json index cd3f84d5c..bea2a5591 100644 --- a/src/app/i18n/locales/it.json +++ b/src/app/i18n/locales/it.json @@ -2121,7 +2121,7 @@ "free": "Ottieni fino a 10 GB gratis", "manageBilling": "Gestisci la fatturazione", "freeForever": "Gratis per sempre", - "billedAnnually": "fatturato annualmente", + "billedAnnually": "/mese fatturato annualmente", "billedMonthly": "fatturato mensilmente", "year": "Anno", "month": "Mese", @@ -2166,7 +2166,8 @@ "Invita, condividi e collabora", "VPN Crittografata", "Antivirus", - "Cleaner (Pulitore)" + "Cleaner (Pulitore)", + "Meet" ] }, "5TB": { diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json index f186a2495..48431d01a 100644 --- a/src/app/i18n/locales/ru.json +++ b/src/app/i18n/locales/ru.json @@ -2029,7 +2029,7 @@ "free": "Получите до 10 ГБ бесплатно", "manageBilling": "Управление платежами", "freeForever": "Бесплатно навсегда", - "billedAnnually": "оплачиваемый ежегодно", + "billedAnnually": "/мес. при годовой оплате", "billedMonthly": "оплачиваемый ежемесячно", "year": "Год", "month": "Месяц", @@ -2074,7 +2074,8 @@ "Приглашение, обмен и совместная работа", "Зашифрованный VPN", "Антивирус", - "Cleaner (Очиститель)" + "Cleaner (Очиститель)", + "Meet" ] }, "5TB": { diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json index 7c076b527..b49681e19 100644 --- a/src/app/i18n/locales/tw.json +++ b/src/app/i18n/locales/tw.json @@ -2018,7 +2018,7 @@ "free": "免費獲取最多 10GB", "manageBilling": "管理帳單", "freeForever": "永久免費", - "billedAnnually": "按年計費", + "billedAnnually": "/月 按年計費", "billedMonthly": "按月計費", "year": "年", "month": "月", @@ -2063,7 +2063,8 @@ "邀請、共享和協作", "加密 VPN", "防毒軟體", - "清理工具 (Cleaner)" + "清理工具 (Cleaner)", + "Meet" ], "comingSoonFeatures": [] }, diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json index 7d20a683b..1d836a020 100644 --- a/src/app/i18n/locales/zh.json +++ b/src/app/i18n/locales/zh.json @@ -2056,7 +2056,7 @@ "free": "免费获取最多 10GB", "manageBilling": "管理账单", "freeForever": "永久免费", - "billedAnnually": "按年计费", + "billedAnnually": "/月 按年计费", "billedMonthly": "按月计费", "year": "年", "month": "月", @@ -2101,7 +2101,8 @@ "邀请、共享和协作", "加密 VPN", "杀毒软件", - "清理工具 (Cleaner)" + "清理工具 (Cleaner)", + "Meet" ] }, "5TB": { diff --git a/src/views/Checkout/utils/formatPrice.test.ts b/src/views/Checkout/utils/formatPrice.test.ts index 6e45710fd..f200c614d 100644 --- a/src/views/Checkout/utils/formatPrice.test.ts +++ b/src/views/Checkout/utils/formatPrice.test.ts @@ -6,12 +6,14 @@ describe('Formatting the price to have 2 decimals', () => { expect(formatPrice(10)).toBe('10'); }); - it('When the user has a price with 1 decimal, the function returns 2 decimals (100.5 -> 100.50)', () => { - expect(formatPrice(100.5)).toBe('100.50'); - }); + describe('The value has less or exactly 2 decimals', () => { + test('When the user has a price with 1 decimal, the function returns 2 decimals (100.5 -> 100.50)', () => { + expect(formatPrice(100.5)).toBe('100.50'); + }); - it('When the user has a price with 2 decimals, the function returns 2 decimals (99.99 -> 99.99)', () => { - expect(formatPrice(99.99)).toBe('99.99'); + test('When the user has a price with 2 decimals, the function returns 2 decimals (99.99 -> 99.99)', () => { + expect(formatPrice(99.99)).toBe('99.99'); + }); }); it('When the price is just 1 decimal and it is a 0, then the function returns without .00', () => { diff --git a/src/views/NewSettings/components/Sections/Account/Plans/components/PlanCard.tsx b/src/views/NewSettings/components/Sections/Account/Plans/components/PlanCard.tsx index 24ed1830b..c68e84743 100644 --- a/src/views/NewSettings/components/Sections/Account/Plans/components/PlanCard.tsx +++ b/src/views/NewSettings/components/Sections/Account/Plans/components/PlanCard.tsx @@ -48,7 +48,7 @@ const PlanCard = ({ isLoading, disableActionButton, }: PlanCardProps) => { - const userText = ' ' + t('preferences.account.plans.billedAnnually'); + const userText = t('preferences.account.plans.billedAnnually'); return (