diff --git a/.changeset/eager-numbers-relate.md b/.changeset/eager-numbers-relate.md new file mode 100644 index 00000000..6d59e8a8 --- /dev/null +++ b/.changeset/eager-numbers-relate.md @@ -0,0 +1,5 @@ +--- +"@godaddy/react": patch +--- + +Use formatCurrency util to respect all currency precisions diff --git a/packages/react/src/components/checkout/form/checkout-form.tsx b/packages/react/src/components/checkout/form/checkout-form.tsx index e0381b53..0fcf874b 100644 --- a/packages/react/src/components/checkout/form/checkout-form.tsx +++ b/packages/react/src/components/checkout/form/checkout-form.tsx @@ -38,6 +38,7 @@ import { ShippingMethodForm } from '@/components/checkout/shipping/shipping-meth import { Target } from '@/components/checkout/target/target'; import { TipsForm } from '@/components/checkout/tips/tips-form'; import { DraftOrderTotals } from '@/components/checkout/totals/totals'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; import { Accordion, AccordionContent, @@ -106,16 +107,16 @@ export function CheckoutForm({ const { data: order } = draftOrder; // Order summary calculations - const subtotal = (totals?.subTotal?.value || 0) / 100; - const orderDiscount = (totals?.discountTotal?.value || 0) / 100; + const subtotal = totals?.subTotal?.value || 0; + const orderDiscount = totals?.discountTotal?.value || 0; const shipping = - (order?.shippingLines?.reduce( + order?.shippingLines?.reduce( (sum, line) => sum + (line?.amount?.value || 0), 0 - ) || 0) / 100; - const taxTotal = (totals?.taxTotal?.value || 0) / 100; - const orderTotal = (totals?.total?.value || 0) / 100; - const tipTotal = (tipAmount || 0) / 100; + ) || 0; + const taxTotal = totals?.taxTotal?.value || 0; + const orderTotal = totals?.total?.value || 0; + const tipTotal = tipAmount || 0; const currencyCode = totals?.total?.currencyCode || 'USD'; const itemCount = items.reduce((sum, item) => sum + (item?.quantity || 0), 0); @@ -427,10 +428,7 @@ export function CheckoutForm({ {t.totals.orderSummary} - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(orderTotal)} + {formatCurrency({ amount: orderTotal, currencyCode })} diff --git a/packages/react/src/components/checkout/line-items/line-items.tsx b/packages/react/src/components/checkout/line-items/line-items.tsx index 9bb14866..c8b1b216 100644 --- a/packages/react/src/components/checkout/line-items/line-items.tsx +++ b/packages/react/src/components/checkout/line-items/line-items.tsx @@ -3,6 +3,7 @@ import { Image } from 'lucide-react'; import { useGoDaddyContext } from '@/godaddy-provider'; import type { SKUProduct } from '@/types'; +import { formatCurrency } from '../utils/format-currency'; export interface Note { content: string | null; @@ -129,10 +130,10 @@ export function DraftOrderLineItems({
- {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(item.originalPrice * item.quantity)} + {formatCurrency({ + amount: item.originalPrice * item.quantity, + currencyCode, + })}
diff --git a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx index 8ada5640..de00d7fd 100644 --- a/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx +++ b/packages/react/src/components/checkout/payment/checkout-buttons/express/godaddy.tsx @@ -34,6 +34,7 @@ import { filterAndSortShippingMethods } from '@/components/checkout/shipping/uti import { useGetShippingMethodByAddress } from '@/components/checkout/shipping/utils/use-get-shipping-methods'; import { useGetTaxes } from '@/components/checkout/taxes/utils/use-get-taxes'; import { mapOrderToFormValues } from '@/components/checkout/utils/checkout-transformers'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; import { Skeleton } from '@/components/ui/skeleton'; import { useGoDaddyContext } from '@/godaddy-provider'; import { GraphQLErrorWithCodes } from '@/lib/graphql-with-errors'; @@ -100,7 +101,13 @@ export function ExpressCheckoutButton() { type: 'SHIPPING' as const, subtotalPrice: { currencyCode: currency, - value: Number(amount) * 100 || 0, + value: Number( + formatCurrency({ + amount: Number(amount) || 0, + currencyCode: currency, + isInCents: false, + }) + ), }, }, ], @@ -133,10 +140,10 @@ export function ExpressCheckoutButton() { }); const methods = sortedMethods?.map(method => { - const shippingMethodPrice = new Intl.NumberFormat('en-us', { - style: 'currency', - currency: method.cost?.currencyCode || 'USD', - }).format((method.cost?.value || 0) / 100); + const shippingMethodPrice = formatCurrency({ + amount: method.cost?.value || 0, + currencyCode: method.cost?.currencyCode || 'USD', + }); return { id: method?.displayName?.replace(/\s+/g, '-')?.toLowerCase(), @@ -144,7 +151,11 @@ export function ExpressCheckoutButton() { detail: method.description ? `(${method.description}) ${shippingMethodPrice}` : `${shippingMethodPrice}`, - amount: ((method.cost?.value || 0) / 100).toString(), + amount: formatCurrency({ + amount: method.cost?.value || 0, + currencyCode, + returnRaw: true, + }), }; }); @@ -173,7 +184,11 @@ export function ExpressCheckoutButton() { // Always add the discount line item, using state variables directly updatedLineItems.push({ label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + returnRaw: true, + }), isPending: false, }); @@ -194,7 +209,11 @@ export function ExpressCheckoutButton() { couponCode: { code: appliedCouponCode, label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + returnRaw: true, + }), }, }; } else { @@ -328,7 +347,11 @@ export function ExpressCheckoutButton() { couponConfig = { code: appliedCouponCode, label: t.totals.discount, - amount: (priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + returnRaw: true, + }), }; } @@ -439,7 +462,13 @@ export function ExpressCheckoutButton() { { subTotal: { currencyCode: currencyCode, - value: Number(selectedMethod.amount) * 100, + value: Number( + formatCurrency({ + amount: Number(selectedMethod.amount), + currencyCode, + isInCents: false, + }) + ), }, name: selectedMethod.name, }, @@ -520,14 +549,22 @@ export function ExpressCheckoutButton() { if (godaddyTotals.shipping.value > 0) { finalLineItems.push({ label: 'Shipping', - amount: (godaddyTotals.shipping.value / 100).toString(), + amount: formatCurrency({ + amount: godaddyTotals.shipping.value, + currencyCode, + returnRaw: true, + }), }); } if (godaddyTotals.taxes.value > 0) { finalLineItems.push({ label: t.totals.estimatedTaxes, - amount: (godaddyTotals.taxes.value / 100).toString(), + amount: formatCurrency({ + amount: godaddyTotals.taxes.value, + currencyCode, + returnRaw: true, + }), }); } @@ -555,7 +592,11 @@ export function ExpressCheckoutButton() { let shippingLines: ReturnType; if (shippingAddress && shippingMethod) { const selectedMethodInfo = { - amount: (godaddyTotals.shipping.value / 100).toString(), + amount: formatCurrency({ + amount: godaddyTotals.shipping.value, + currencyCode, + returnRaw: true, + }), name: shippingMethod, }; shippingLines = convertAddressToShippingLines( @@ -580,21 +621,33 @@ export function ExpressCheckoutButton() { if (godaddyTotals.shipping.value > 0) { finalLineItems.push({ label: t.totals.shipping, - amount: (godaddyTotals.shipping.value / 100).toString(), + amount: formatCurrency({ + amount: godaddyTotals.shipping.value, + currencyCode, + returnRaw: true, + }), }); } if (godaddyTotals.taxes.value > 0) { finalLineItems.push({ label: t.totals.estimatedTaxes, - amount: (godaddyTotals.taxes.value / 100).toString(), + amount: formatCurrency({ + amount: godaddyTotals.taxes.value, + currencyCode, + returnRaw: true, + }), }); } // Add the discount line item finalLineItems.push({ label: t.totals.discount, - amount: (-adjustment / 100).toString(), + amount: formatCurrency({ + amount: -adjustment, + currencyCode, + returnRaw: true, + }), }); // Calculate the total amount @@ -611,7 +664,11 @@ export function ExpressCheckoutButton() { couponCode: { code: couponCode, label: t.totals.discount, - amount: (-adjustment / 100).toString(), + amount: formatCurrency({ + amount: -adjustment, + currencyCode, + returnRaw: true, + }), }, }; } else { @@ -844,7 +901,13 @@ export function ExpressCheckoutButton() { ...value, shipping: { currencyCode: 'USD', - value: Number(shippingAmount) * 100 || 0, + value: Number( + formatCurrency({ + amount: Number(shippingAmount) || 0, + currencyCode, + isInCents: false, + }) + ), }, })); @@ -897,7 +960,11 @@ export function ExpressCheckoutButton() { if (taxesResult?.value) { poyntLineItems.push({ label: t.totals.estimatedTaxes, - amount: (taxesResult.value / 100).toString(), + amount: formatCurrency({ + amount: taxesResult.value, + currencyCode, + returnRaw: true, + }), isPending: false, }); setGoDaddyTotals(value => ({ @@ -925,7 +992,11 @@ export function ExpressCheckoutButton() { if (priceAdjustment && appliedCouponCode) { poyntLineItems.push({ label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + returnRaw: true, + }), isPending: false, }); } @@ -949,7 +1020,11 @@ export function ExpressCheckoutButton() { updatedOrder.couponCode = { code: appliedCouponCode, label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + returnRaw: true, + }), }; } @@ -979,7 +1054,13 @@ export function ExpressCheckoutButton() { ...value, shipping: { currencyCode: 'USD', - value: Number(methods?.[0]?.amount) * 100 || 0, + value: Number( + formatCurrency({ + amount: Number(methods?.[0]?.amount) || 0, + currencyCode, + isInCents: false, + }) + ), }, })); @@ -1060,7 +1141,11 @@ export function ExpressCheckoutButton() { if (taxesResult?.value) { poyntLineItems.push({ label: t.totals.estimatedTaxes, - amount: (taxesResult.value / 100).toString(), + amount: formatCurrency({ + amount: taxesResult.value, + currencyCode, + returnRaw: true, + }), isPending: false, }); setGoDaddyTotals(value => ({ @@ -1094,7 +1179,11 @@ export function ExpressCheckoutButton() { if (priceAdjustment && appliedCouponCode) { poyntLineItems.push({ label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + returnRaw: true, + }), isPending: false, }); } @@ -1119,7 +1208,11 @@ export function ExpressCheckoutButton() { updatedOrder.couponCode = { code: appliedCouponCode, label: appliedCouponCode || 'Discount', - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + returnRaw: true, + }), }; } else { updatedOrder.couponCode = { diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index c753e7be..9c864831 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -34,6 +34,7 @@ import { DraftOrderTotals, type DraftOrderTotalsProps, } from '@/components/checkout/totals/totals'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; import { Accordion, AccordionContent, @@ -447,10 +448,10 @@ export function PaymentForm( {t.totals.orderSummary} - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: props.currencyCode, - }).format(props.total || 0)} + {formatCurrency({ + amount: props.total || 0, + currencyCode: props.currencyCode || 'USD', + })} diff --git a/packages/react/src/components/checkout/payment/utils/use-build-payment-request.ts b/packages/react/src/components/checkout/payment/utils/use-build-payment-request.ts index 144309ce..9ad3793b 100644 --- a/packages/react/src/components/checkout/payment/utils/use-build-payment-request.ts +++ b/packages/react/src/components/checkout/payment/utils/use-build-payment-request.ts @@ -8,6 +8,7 @@ import { } from '@/components/checkout/order/use-draft-order'; import { useDraftOrderProductsMap } from '@/components/checkout/order/use-draft-order-products'; import { mapSkusToItemsDisplay } from '@/components/checkout/utils/checkout-transformers'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; // Apple Pay request interface export interface ApplePayRequest { @@ -194,15 +195,45 @@ export function useBuildPaymentRequest(): { ); // Convert amounts from cents to dollars for display - const subtotal = (totals?.subTotal?.value || 0) / 100; - const tax = (totals?.taxTotal?.value || 0) / 100; - const shipping = - (order?.shippingLines?.reduce( - (sum, line) => sum + (line?.amount?.value || 0), - 0 - ) || 0) / 100; - const discount = (totals?.discountTotal?.value || 0) / 100; - const total = (totals?.total?.value || 0) / 100; + const subtotal = Number( + formatCurrency({ + amount: totals?.subTotal?.value || 0, + currencyCode, + returnRaw: true, + }) + ); + const tax = Number( + formatCurrency({ + amount: totals?.taxTotal?.value || 0, + currencyCode, + returnRaw: true, + }) + ); + const shipping = Number( + formatCurrency({ + amount: + order?.shippingLines?.reduce( + (sum, line) => sum + (line?.amount?.value || 0), + 0 + ) || 0, + currencyCode, + returnRaw: true, + }) + ); + const discount = Number( + formatCurrency({ + amount: totals?.discountTotal?.value || 0, + currencyCode, + returnRaw: true, + }) + ); + const total = Number( + formatCurrency({ + amount: totals?.total?.value || 0, + currencyCode, + returnRaw: true, + }) + ); const countryCode = useMemo( () => session?.shipping?.originAddress?.countryCode || 'US', @@ -254,52 +285,38 @@ export function useBuildPaymentRequest(): { merchantCapabilities: ['supports3DS'], total: { label: 'Order Total', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(total), + amount: total.toFixed(2), type: 'final', }, lineItems: [ ...(items || []).map(lineItem => ({ label: lineItem?.name || '', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format((lineItem?.originalPrice || 0) * (lineItem?.quantity || 0)), + amount: formatCurrency({ + amount: (lineItem?.originalPrice || 0) * (lineItem?.quantity || 0), + currencyCode, + returnRaw: true, + }), type: 'LINE_ITEM', status: 'FINAL', })), { label: 'Subtotal', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(subtotal), + amount: subtotal.toFixed(2), type: 'final', }, { label: 'Tax', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(tax), + amount: tax.toFixed(2), type: 'final', }, { label: 'Shipping', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(shipping), + amount: shipping.toFixed(2), type: 'final', }, { label: 'Discount', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(-1 * discount), + amount: (-1 * discount).toFixed(2), type: 'final', }, ].filter(item => Number.parseFloat(item.amount) !== 0), @@ -338,16 +355,20 @@ export function useBuildPaymentRequest(): { }, transactionInfo: { totalPriceStatus: 'FINAL', - totalPrice: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(total), + totalPrice: total.toFixed(2), totalPriceLabel: 'Total', currencyCode, displayItems: [ ...(items || []).map(lineItem => ({ label: lineItem?.name || '', - price: (lineItem?.originalPrice || 0) * (lineItem?.quantity || 0), + price: Number( + formatCurrency({ + amount: + (lineItem?.originalPrice || 0) * (lineItem?.quantity || 0), + currencyCode, + returnRaw: true, + }) + ), type: 'LINE_ITEM', status: 'FINAL', })), @@ -411,7 +432,11 @@ export function useBuildPaymentRequest(): { name: lineItem?.name || '', unit_amount: { currency_code: currencyCode, - value: (lineItem?.originalPrice || 0).toFixed(2), + value: formatCurrency({ + amount: lineItem?.originalPrice || 0, + currencyCode, + returnRaw: true, + }), }, quantity: (lineItem?.quantity || 1).toString(), })), @@ -500,15 +525,17 @@ export function useBuildPaymentRequest(): { const poyntExpressRequest: PoyntExpressRequest = { total: { label: 'Order Total', - amount: subtotal.toString(), + amount: subtotal.toFixed(2), }, lineItems: [ ...(items || []).map(lineItem => { return { label: lineItem?.name || '', - amount: ( - (lineItem?.originalPrice || 0) * (lineItem?.quantity || 1) - ).toString(), + amount: formatCurrency({ + amount: (lineItem?.originalPrice || 0) * (lineItem?.quantity || 1), + currencyCode, + returnRaw: true, + }), }; }), ], @@ -517,15 +544,17 @@ export function useBuildPaymentRequest(): { const poyntStandardRequest: PoyntStandardRequest = { total: { label: 'Order Total', - amount: total.toString(), + amount: total.toFixed(2), }, lineItems: [ ...(items || []).map(lineItem => { return { label: lineItem?.name || '', - amount: ( - (lineItem?.originalPrice || 0) * (lineItem?.quantity || 1) - ).toString(), + amount: formatCurrency({ + amount: (lineItem?.originalPrice || 0) * (lineItem?.quantity || 1), + currencyCode, + returnRaw: true, + }), }; }), { diff --git a/packages/react/src/components/checkout/shipping/shipping-method.tsx b/packages/react/src/components/checkout/shipping/shipping-method.tsx index c9023d20..51bb3d84 100644 --- a/packages/react/src/components/checkout/shipping/shipping-method.tsx +++ b/packages/react/src/components/checkout/shipping/shipping-method.tsx @@ -14,6 +14,7 @@ import { ShippingMethodSkeleton } from '@/components/checkout/shipping/shipping- import { filterAndSortShippingMethods } from '@/components/checkout/shipping/utils/filter-shipping-methods'; import { useApplyShippingMethod } from '@/components/checkout/shipping/utils/use-apply-shipping-method'; import { useDraftOrderShippingMethods } from '@/components/checkout/shipping/utils/use-draft-order-shipping-methods'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { useGoDaddyContext } from '@/godaddy-provider'; @@ -259,10 +260,11 @@ export function ShippingMethodForm() { {t.general.free} ) : ( - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: shippingMethods[0]?.cost?.currencyCode || 'USD', - }).format((shippingMethods[0]?.cost?.value || 0) / 100)} + {formatCurrency({ + amount: shippingMethods[0]?.cost?.value || 0, + currencyCode: + shippingMethods[0]?.cost?.currencyCode || 'USD', + })} )} @@ -301,10 +303,10 @@ export function ShippingMethodForm() { {t.general.free} ) : ( - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: method?.cost?.currencyCode || 'USD', - }).format((method?.cost?.value || 0) / 100)} + {formatCurrency({ + amount: method?.cost?.value || 0, + currencyCode: method?.cost?.currencyCode || 'USD', + })} )} diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index bfc0aeab..360a0eda 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useFormContext } from 'react-hook-form'; import { useCheckoutContext } from '@/components/checkout/checkout'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; import { Button } from '@/components/ui/button'; import { FormControl, @@ -30,14 +31,6 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { return Math.round(((total * percentage) / 100) * 100); }; - const formatCurrency = (amount: number): string => { - // Convert from cents to dollars before formatting - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: currencyCode || 'USD', - }).format(amount / 100); - }; - const handlePercentageSelect = (percentage: number) => { const tipAmount = calculateTipAmount(percentage); form.setValue('tipAmount', tipAmount); @@ -116,7 +109,10 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { > {percentage}% - {formatCurrency(calculateTipAmount(percentage))} + {formatCurrency({ + amount: calculateTipAmount(percentage), + currencyCode: currencyCode || 'USD', + })} ))} diff --git a/packages/react/src/components/checkout/totals/totals.tsx b/packages/react/src/components/checkout/totals/totals.tsx index ca4c4751..f1e91367 100644 --- a/packages/react/src/components/checkout/totals/totals.tsx +++ b/packages/react/src/components/checkout/totals/totals.tsx @@ -1,5 +1,6 @@ import { DiscountStandalone } from '@/components/checkout/discount/discount-standalone'; import { TotalLineItemSkeleton } from '@/components/checkout/totals/totals-skeleton'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; import { useGoDaddyContext } from '@/godaddy-provider'; export interface DraftOrderTotalsProps { @@ -39,12 +40,7 @@ function TotalLineItem({ {description} ) : null} - - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(value)} - + {formatCurrency({ amount: value, currencyCode })} ); } @@ -143,10 +139,7 @@ export function DraftOrderTotals({ {currencyCode}{' '} - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(total)} + {formatCurrency({ amount: total, currencyCode })} diff --git a/packages/react/src/components/checkout/utils/checkout-transformers.ts b/packages/react/src/components/checkout/utils/checkout-transformers.ts index cb107ad0..02b92f1d 100644 --- a/packages/react/src/components/checkout/utils/checkout-transformers.ts +++ b/packages/react/src/components/checkout/utils/checkout-transformers.ts @@ -147,15 +147,12 @@ export function mapSkusToItemsDisplay( image: orderItem.details?.productAssetUrl || skuDetails?.mediaUrls?.[0], quantity: orderItem.quantity || 0, originalPrice: - (orderItem.totals?.subTotal?.value ?? 0) / - (orderItem.quantity || 0) / - 100, + (orderItem.totals?.subTotal?.value ?? 0) / (orderItem.quantity || 0), price: - ((orderItem.totals?.subTotal?.value ?? 0) + - (orderItem.totals?.feeTotal?.value ?? 0) - - // (orderItem.totals?.taxTotal?.value ?? 0) - // do we need taxTotal here? - (orderItem.totals?.discountTotal?.value ?? 0)) / - 100, + (orderItem.totals?.subTotal?.value ?? 0) + + (orderItem.totals?.feeTotal?.value ?? 0) - + // (orderItem.totals?.taxTotal?.value ?? 0) - // do we need taxTotal here? + (orderItem.totals?.discountTotal?.value ?? 0), notes: orderItem.notes ?.filter( note => diff --git a/packages/react/src/components/checkout/utils/format-currency.ts b/packages/react/src/components/checkout/utils/format-currency.ts new file mode 100644 index 00000000..1b71c3e0 --- /dev/null +++ b/packages/react/src/components/checkout/utils/format-currency.ts @@ -0,0 +1,90 @@ +/** + * Currency configuration map with symbols and decimal precision. + */ +export const currencyConfigs: Record< + string, + { symbol: string; precision: number; pattern?: string } +> = { + AUD: { symbol: '$', precision: 2 }, + CAD: { symbol: '$', precision: 2 }, + HKD: { symbol: '$', precision: 2 }, + SGD: { symbol: '$', precision: 2 }, + NZD: { symbol: '$', precision: 2 }, + USD: { symbol: '$', precision: 2 }, + VND: { symbol: '₫', precision: 0 }, + EUR: { symbol: '€', precision: 2 }, + GBP: { symbol: '£', precision: 2 }, + ARS: { symbol: '$', precision: 2 }, + CLP: { symbol: '$', precision: 0 }, + COP: { symbol: '$', precision: 2 }, + PHP: { symbol: '₱', precision: 2 }, + MXN: { symbol: '$', precision: 2 }, + BRL: { symbol: 'R$', precision: 2 }, + INR: { symbol: '₹', precision: 2 }, + IDR: { symbol: 'Rp', precision: 2 }, + PEN: { symbol: 'S/', precision: 2 }, + AED: { symbol: 'د.إ', precision: 2, pattern: '#!' }, + ILS: { symbol: '₪', precision: 2 }, + TRY: { symbol: '₺', precision: 2 }, + ZAR: { symbol: 'R', precision: 2 }, + CNY: { symbol: '¥', precision: 2 }, +}; + +export interface FormatCurrencyOptions { + /** Numeric amount to format or convert */ + amount: number; + /** ISO 4217 currency code (e.g. 'USD', 'VND', 'CLP') */ + currencyCode: string; + /** Optional locale, defaults to 'en-US' */ + locale?: string; + /** + * Indicates whether the input is already in cents (minor units). + * - true → format to currency string (default) + * - false → convert to minor units and return as string + */ + isInCents?: boolean; + /** + * Return raw numeric value without currency symbol. + * - true → returns "10.00" instead of "$10.00" + * - false → returns full currency string (default) + */ + returnRaw?: boolean; +} + +/** + * Formats or converts a currency amount. + * + * - When `isInCents = true` (default): returns formatted string like "$123.45" + * - When `isInCents = false`: returns string representing minor units like "12345" + * - When `returnRaw = true`: returns numeric value without currency symbol like "123.45" + */ +export function formatCurrency({ + amount, + currencyCode, + locale = 'en-US', + isInCents = true, + returnRaw = false, +}: FormatCurrencyOptions): string { + const config = currencyConfigs[currencyCode]; + + if (!config) { + return amount.toString(); + } + + const { precision } = config; + + if (!isInCents) { + // Convert major units to minor units and return as string + return Math.round(amount * Math.pow(10, precision)).toString(); + } + + // Format value already in minor units + const value = amount / Math.pow(10, precision); + + return new Intl.NumberFormat(locale, { + style: returnRaw ? 'decimal' : 'currency', + currency: returnRaw ? undefined : currencyCode, + minimumFractionDigits: precision, + maximumFractionDigits: precision, + }).format(value); +}