Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@
},
"apply": "Anwenden",
"taxes": "Steuern",
"annualBillingTemplate": "Jahresplan, monatlich abgerechnet mit {{price}}{{currency}}/Monat für 12 Monate"
"annualBillingTemplate": "Jahresplan, Abrechnung von {{priceNow}}{{currency}} für den ersten Monat, dann Verlängerung für {{price}}{{currency}}/Monat"
},
"confirmCryptoPayment": {
"title": "Zahlung bestätigen",
Expand Down
2 changes: 1 addition & 1 deletion src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 {{priceNow}}{{currency}} for the first month, then renews at {{price}}{{currency}}/month"
},
"confirmCryptoPayment": {
"title": "Confirm the payment",
Expand Down
2 changes: 1 addition & 1 deletion src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 {{priceNow}}{{currency}} el primer mes, luego se renueva a {{price}}{{currency}}/mes"
},
"confirmCryptoPayment": {
"title": "Confirmar el pago",
Expand Down
2 changes: 1 addition & 1 deletion src/app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@
},
"apply": "Appliquer",
"taxes": "Taxes",
"annualBillingTemplate": "Plan annuel, facturé mensuellement à {{price}}{{currency}}/mois pendant 12 mois"
"annualBillingTemplate": "Plan annuel, facturé {{priceNow}}{{currency}} le premier mois, puis renouvelé à {{price}}{{currency}}/mois"
},
"confirmCryptoPayment": {
"title": "Confirmer le paiement",
Expand Down
2 changes: 1 addition & 1 deletion src/app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@
},
"apply": "Applica",
"taxes": "Tasse",
"annualBillingTemplate": "Piano annuale, fatturato mensilmente a {{price}}{{currency}}/mese per 12 mesi"
"annualBillingTemplate": "Piano annuale, fatturato a {{priceNow}}{{currency}} per il primo mese, poi si rinnova a {{price}}{{currency}}/mese"
},
"confirmCryptoPayment": {
"title": "Conferma il pagamento",
Expand Down
2 changes: 1 addition & 1 deletion src/app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@
},
"apply": "Применить",
"taxes": "Налоги",
"annualBillingTemplate": "Годовой план, ежемесячная оплата {{price}}{{currency}}/мес. в течение 12 месяцев"
"annualBillingTemplate": "Годовой план, оплата {{priceNow}}{{currency}} за первый месяц, далее продлевается за {{price}}{{currency}}/мес."
},
"confirmCryptoPayment": {
"title": "Подтвердить платеж",
Expand Down
2 changes: 1 addition & 1 deletion src/app/i18n/locales/tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@
},
"apply": "應用",
"taxes": "稅金",
"annualBillingTemplate": "年度計劃,每月 {{price}}{{currency}}/月,持續 12 個月"
"annualBillingTemplate": "年度計劃,第一個月費用為 {{priceNow}}{{currency}},之後以 {{price}}{{currency}}/月續訂"
},
"confirmCryptoPayment": {
"title": "確認付款",
Expand Down
2 changes: 1 addition & 1 deletion src/app/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@
},
"apply": "应用",
"taxes": "税费",
"annualBillingTemplate": "年度计划,每月 {{price}}{{currency}}/月,持续 12 个月"
"annualBillingTemplate": "年度计划,第一个月费用为 {{priceNow}}{{currency}},之后以 {{price}}{{currency}}/月续订"
},
"confirmCryptoPayment": {
"title": "确认付款",
Expand Down
16 changes: 7 additions & 9 deletions src/views/Checkout/components/CheckoutProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,11 @@ export const CheckoutProductCard = ({
const normalPriceAmount = priceData.decimalAmount;

const totalLabel = translate('checkout.productCard.total');
const renewalPeriodLabel = `${translate('checkout.productCard.renewalPeriod.renewsAt')}
${currencySymbol}${normalPriceAmount}/${translate(
`checkout.productCard.renewalPeriod.${priceData.interval}`,
)}`;

const planAmountWithoutTaxes = getProductAmount(priceData.decimalAmount, 1, couponCodeData);
const totalAmountFormatted = formatPrice(taxesData.decimalAmountWithTax);
const derivedTax = Math.max(0, Number(totalAmountFormatted) - Number(planAmountWithoutTaxes));
const derivedTaxFormatted = formatPrice(derivedTax);

const discountPercentage =
couponCodeData?.amountOff && couponCodeData?.amountOff < taxesData.amountWithTax
Expand Down Expand Up @@ -109,12 +108,12 @@ export const CheckoutProductCard = ({
</p>
</div>

{taxesData.decimalTax > 0 && (
{Number(derivedTaxFormatted) > 0 && (
<div className="flex flex-row items-center justify-between text-gray-100">
<p className="font-medium">{translate('checkout.productCard.taxes')}</p>
<p className="font-semibold">
{currencySymbol}
{taxesData.decimalTax}
{derivedTaxFormatted}
</p>
</div>
)}
Expand Down Expand Up @@ -163,7 +162,7 @@ export const CheckoutProductCard = ({
<p>{totalLabel}</p>
<p>
{currencySymbol}
{formatPrice(taxesData.decimalAmountWithTax)}
{totalAmountFormatted}
</p>
</div>

Expand Down Expand Up @@ -254,11 +253,10 @@ export const CheckoutProductCard = ({
)}
</div>
</div>
{couponCodeData && priceData.interval !== 'lifetime' && <p className="text-gray-60">{renewalPeriodLabel}</p>}
{showHardcodedRenewal && <p className="text-gray-60">{showHardcodedRenewal}</p>}
{priceData.interval === 'month' && (
<p className="text-gray-60">
{translate('checkout.productCard.annualBillingTemplate', {
priceNow: totalAmountFormatted,
price: normalPriceAmount,
currency: currencySymbol,
})}
Expand Down
3 changes: 2 additions & 1 deletion src/views/Checkout/components/CryptoPaymentDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useTranslationContext } from 'app/i18n/provider/TranslationProvider';
import notificationsService, { ToastType } from 'app/notifications/services/notifications.service';
import checkoutService from '../services/checkout.service';
import { Currency } from '../types';
import { formatPrice } from '../utils/formatPrice';
import { useEffect, useState } from 'react';
import { errorService } from 'services';

Expand Down Expand Up @@ -166,7 +167,7 @@ export const CryptoPaymentDialog = () => {
</p>
<div className="flex flex-row gap-3 items-center">
<p className="text-gray-100 dark:text-white text-lg font-normal">
{fiat.amount} {Currency[fiat.currency]}
{formatPrice(fiat.amount)} {Currency[fiat.currency]}
</p>
</div>
</div>
Expand Down
9 changes: 5 additions & 4 deletions src/views/Checkout/utils/formatPrice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { formatPrice } from './formatPrice';

describe('Formatting the price to have 2 decimals', () => {
it('When the price does not have decimals, the function returns it again', () => {
it('When the price does not have decimals, the function returns it without .00', () => {
expect(formatPrice(10)).toBe('10');
});

Expand All @@ -16,14 +16,15 @@ describe('Formatting the price to have 2 decimals', () => {
});
});

it('When the price is just 1 decimal and it is a 0, then the function returns an integer (without 0s)', () => {
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 with 2 decimals (rounded, 10.456 -> 10.46 - 10.001 -> 10)', () => {
expect(formatPrice(10.456)).toBe('10.46');
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');
expect(formatPrice(10.001)).toBe('10');
expect(formatPrice(1.999)).toBe('1.99');
});
});

Expand Down
5 changes: 3 additions & 2 deletions src/views/Checkout/utils/formatPrice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const formatPrice = (price: number) => {
const formattedAmount = Number(price.toFixed(2));
return Number.isInteger(formattedAmount) ? formattedAmount.toString() : price.toFixed(2);
const truncated = Math.floor(Number(price.toFixed(8)) * 100) / 100;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using toFixed(8) here? It looks arbitrary and makes the intent unclear.
If the goal is simply truncating to 2 decimals, Math.floor(price * 100) / 100 should be enough.
If this is a workaround for floating-point precision issues, it would be good to document it explicitly or use a more deterministic decimal handling approach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Math.floor(price * 100) / 100 alone is unsafe. If price is the result of floating-point arithmetic, you can get values like 0.2999999999999 where * 100 gives 29.9999, and Math.floor then truncates to 29 instead of 30,a wrong result for what we want right now. The toFixed(8) neutralizes that noise by rounding to 8 decimal places first, resolving the imprecision before the floor. The alternative Math.floor(price * 100) / 100 would reproduce that exact bug for computed prices.

const formatted = truncated.toFixed(2);
return formatted.endsWith('.00') ? String(truncated) : formatted;
};
Loading