From 1220dc698f86b9a2cfa42adba4b58dc8ca63dda8 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Mon, 10 Nov 2025 12:46:52 -0600 Subject: [PATCH 1/8] keep curreny in minor units and concert using new currenyFormat util for all currencies --- .../checkout/form/checkout-form.tsx | 39 +-- .../checkout/line-items/line-items.tsx | 10 +- .../checkout-buttons/express/godaddy.tsx | 231 +++++++++++++----- .../checkout/payment/payment-form.tsx | 10 +- .../utils/use-build-payment-request.ts | 195 +++++++++++---- .../checkout/shipping/shipping-method.tsx | 19 +- .../components/checkout/tips/tips-form.tsx | 45 ++-- .../src/components/checkout/totals/totals.tsx | 19 +- .../checkout/utils/checkout-transformers.ts | 6 +- .../checkout/utils/format-currency.ts | 90 +++++++ 10 files changed, 486 insertions(+), 178 deletions(-) create mode 100644 packages/react/src/components/checkout/utils/format-currency.ts diff --git a/packages/react/src/components/checkout/form/checkout-form.tsx b/packages/react/src/components/checkout/form/checkout-form.tsx index e0381b53..494b213d 100644 --- a/packages/react/src/components/checkout/form/checkout-form.tsx +++ b/packages/react/src/components/checkout/form/checkout-form.tsx @@ -50,6 +50,7 @@ import { TrackingEventType, track } from '@/tracking/track'; import { CheckoutType, PaymentMethodType } from '@/types'; import { FreePaymentForm } from '../payment/free-payment-form'; import { CustomFormProvider } from './custom-form-provider'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; const deliveryMethodToGridArea: Record = { SHIP: 'shipping', @@ -105,22 +106,21 @@ export function CheckoutForm({ const { data: totals, isLoading: totalsLoading } = draftOrderTotalsQuery; const { data: order } = draftOrder; - // Order summary calculations - const subtotal = (totals?.subTotal?.value || 0) / 100; - const orderDiscount = (totals?.discountTotal?.value || 0) / 100; - const shipping = - (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; + // Order summary calculations - keep all values in minor units + const subtotal = totals?.subTotal?.value || 0; + const orderDiscount = totals?.discountTotal?.value || 0; + const shipping = order?.shippingLines?.reduce( + (sum, line) => sum + (line?.amount?.value || 0), + 0 + ) || 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); const isFree = orderTotal <= 0; - const showExpressButtons = (totals?.subTotal?.value || 0) > 0; + const showExpressButtons = subtotal > 0; useEffect(() => { if (!totalsLoading && isFree) { @@ -136,8 +136,8 @@ export function CheckoutForm({ eventId: eventIds.checkoutStart, type: TrackingEventType.IMPRESSION, properties: { - subtotal: Math.round(subtotal * 100), - total: Math.round(orderTotal * 100), + subtotal: subtotal, + total: orderTotal, itemCount, currencyCode, }, @@ -240,7 +240,7 @@ export function CheckoutForm({ deliveryMethod: data.deliveryMethod, hasShippingAddress: !!data.shippingAddressLine1, hasBillingAddress: !!data.billingAddressLine1, - total: Math.round(orderTotal * 100), + total: orderTotal, }, }); }; @@ -427,10 +427,11 @@ export function CheckoutForm({ {t.totals.orderSummary} - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(orderTotal)} + {formatCurrency({ + amount: totals?.total?.value || 0, + currencyCode, + isInCents: true, + })} 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..35fff0b6 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 '@/components/checkout/utils/format-currency'; export interface Note { content: string | null; @@ -129,10 +130,11 @@ export function DraftOrderLineItems({
- {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(item.originalPrice * item.quantity)} + {formatCurrency({ + amount: item.originalPrice * item.quantity, + currencyCode, + isInCents: true, + })}
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..a973aaec 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'; @@ -58,13 +59,6 @@ export function ExpressCheckoutButton() { const [appliedCouponCode, setAppliedCouponCode] = useState( null ); - const [godaddyTotals, setGoDaddyTotals] = useState<{ - taxes: { currencyCode: string; value: number }; - shipping: { currencyCode: string; value: number }; - }>({ - taxes: { currencyCode: 'USD', value: 0 }, - shipping: { currencyCode: 'USD', value: 0 }, - }); const [shippingAddress, setShippingAddress] = useState
(null); const [shippingMethods, setShippingMethods] = useState(null); @@ -72,13 +66,21 @@ export function ExpressCheckoutButton() { const { poyntExpressRequest } = useBuildPaymentRequest(); const { data: totals } = useDraftOrderTotals(); const { data: draftOrder } = useDraftOrder(); + const currencyCode = totals?.total?.currencyCode || 'USD'; + + const [godaddyTotals, setGoDaddyTotals] = useState<{ + taxes: { currencyCode: string; value: number }; + shipping: { currencyCode: string; value: number }; + }>({ + taxes: { currencyCode: currencyCode, value: 0 }, + shipping: { currencyCode: currencyCode, value: 0 }, + }); const form = useFormContext(); const getShippingMethodsByAddress = useGetShippingMethodByAddress(); const getTaxes = useGetTaxes(); const getPriceAdjustments = useGetPriceAdjustments(); const updateTaxes = useUpdateTaxes(); - const currencyCode = totals?.total?.currencyCode || 'USD'; const countryCode = session?.shipping?.originAddress?.countryCode || 'US'; const confirmCheckout = useConfirmCheckout(); @@ -100,6 +102,7 @@ export function ExpressCheckoutButton() { type: 'SHIPPING' as const, subtotalPrice: { currencyCode: currency, + // Wallet APIs provide amounts in major units (e.g., "10.50"), convert to minor units for our API value: Number(amount) * 100 || 0, }, }, @@ -133,10 +136,11 @@ 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 || currencyCode, + isInCents: true, + }); return { id: method?.displayName?.replace(/\s+/g, '-')?.toLowerCase(), @@ -144,7 +148,13 @@ 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: method.cost?.currencyCode || currencyCode, + isInCents: true, + returnRaw: true, + }), + amountInMinorUnits: method.cost?.value || 0, // Keep original minor unit value }; }); @@ -173,28 +183,43 @@ 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, + isInCents: true, + returnRaw: true, + }), isPending: false, }); - // Calculate the correct total amount that includes the discount - const totalAmount = updatedLineItems.reduce( - (acc, item) => acc + Number(item.amount), - 0 - ); + // Calculate the correct total in minor units + const totalInMinorUnits = + (totals?.subTotal?.value || 0) - priceAdjustment; + + const totalAmount = formatCurrency({ + amount: totalInMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }); expressRequest = { ...expressRequest, lineItems: updatedLineItems, total: { label: t.payment.orderTotal, - amount: totalAmount.toString(), + amount: totalAmount, isPending: false, }, couponCode: { code: appliedCouponCode, label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, }; } else { @@ -328,7 +353,12 @@ export function ExpressCheckoutButton() { couponConfig = { code: appliedCouponCode, label: t.totals.discount, - amount: (priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: priceAdjustment, + currencyCode, + isInCents: true, + returnRaw: true, + }), }; } @@ -431,7 +461,7 @@ export function ExpressCheckoutButton() { const convertAddressToShippingLines = useCallback( ( address: Address | null, - selectedMethod: { amount: string; name: string } + selectedMethod: { amountInMinorUnits: number; name: string } ) => { if (!address) return undefined; @@ -439,7 +469,7 @@ export function ExpressCheckoutButton() { { subTotal: { currencyCode: currencyCode, - value: Number(selectedMethod.amount) * 100, + value: selectedMethod.amountInMinorUnits, }, name: selectedMethod.name, }, @@ -471,8 +501,8 @@ export function ExpressCheckoutButton() { // Reset totals for a fresh calculation setGoDaddyTotals({ - taxes: { currencyCode: 'USD', value: 0 }, - shipping: { currencyCode: 'USD', value: 0 }, + taxes: { currencyCode: currencyCode, value: 0 }, + shipping: { currencyCode: currencyCode, value: 0 }, }); form.reset( @@ -520,27 +550,45 @@ 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, + isInCents: true, + 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, + isInCents: true, + returnRaw: true, + }), }); } - // Calculate the total amount - const totalAmount = finalLineItems - .reduce((acc, item) => acc + Number(item.amount), 0) - .toFixed(2); + // Calculate the total in minor units then format with proper currency precision + const totalInMinorUnits = + (totals?.subTotal?.value || 0) + + godaddyTotals.shipping.value + + godaddyTotals.taxes.value; + + const totalAmount = formatCurrency({ + amount: totalInMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }); updatedOrder = { lineItems: finalLineItems, total: { label: t.payment.orderTotal, - amount: totalAmount.toString(), + amount: totalAmount, }, couponCode: { code: '', @@ -555,7 +603,7 @@ export function ExpressCheckoutButton() { let shippingLines: ReturnType; if (shippingAddress && shippingMethod) { const selectedMethodInfo = { - amount: (godaddyTotals.shipping.value / 100).toString(), + amountInMinorUnits: godaddyTotals.shipping.value, name: shippingMethod, }; shippingLines = convertAddressToShippingLines( @@ -580,38 +628,67 @@ 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, + isInCents: true, + 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, + isInCents: true, + returnRaw: true, + }), }); } // Add the discount line item finalLineItems.push({ label: t.totals.discount, - amount: (-adjustment / 100).toString(), + amount: formatCurrency({ + amount: -adjustment, + currencyCode, + isInCents: true, + returnRaw: true, + }), }); - // Calculate the total amount - const totalAmount = finalLineItems - .reduce((acc, item) => acc + Number(item.amount), 0) - .toFixed(2); + // Calculate the total in minor units then format with proper currency precision + const totalInMinorUnits = + (totals?.subTotal?.value || 0) + + godaddyTotals.shipping.value + + godaddyTotals.taxes.value - + adjustment; + + const totalAmount = formatCurrency({ + amount: totalInMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }); updatedOrder = { lineItems: finalLineItems, total: { label: t.payment.orderTotal, - amount: totalAmount.toString(), + amount: totalAmount, }, couponCode: { code: couponCode, label: t.totals.discount, - amount: (-adjustment / 100).toString(), + amount: formatCurrency({ + amount: -adjustment, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, }; } else { @@ -748,7 +825,7 @@ export function ExpressCheckoutButton() { subTotal: godaddyTotals.shipping, taxTotal: { value: 0, - currencyCode: 'USD', + currencyCode: currencyCode, }, }, }, @@ -833,6 +910,7 @@ export function ExpressCheckoutButton() { // Handle shipping method change if (e.shippingMethod && shippingAddress) { + // Wallet API provides shipping amount in major units (e.g., "10.50") const shippingAmount = e.shippingMethod?.amount; poyntLineItems.push({ @@ -843,7 +921,8 @@ export function ExpressCheckoutButton() { setGoDaddyTotals(value => ({ ...value, shipping: { - currencyCode: 'USD', + currencyCode: currencyCode, + // Convert wallet API amount from major to minor units for internal storage value: Number(shippingAmount) * 100 || 0, }, })); @@ -854,7 +933,8 @@ export function ExpressCheckoutButton() { const shippingLines = convertAddressToShippingLines( shippingAddress, { - amount: shippingAmount || '0', + // Convert wallet API amount from major to minor units for API request + amountInMinorUnits: Number(shippingAmount) * 100 || 0, name: e.shippingMethod?.label || t.totals.shipping, } ); @@ -897,12 +977,20 @@ export function ExpressCheckoutButton() { if (taxesResult?.value) { poyntLineItems.push({ label: t.totals.estimatedTaxes, - amount: (taxesResult.value / 100).toString(), + amount: formatCurrency({ + amount: taxesResult.value, + currencyCode, + isInCents: true, + returnRaw: true, + }), isPending: false, }); setGoDaddyTotals(value => ({ ...value, - taxes: { currencyCode: 'USD', value: taxesResult.value || 0 }, + taxes: { + currencyCode: currencyCode, + value: taxesResult.value || 0, + }, })); } } catch (_error) { @@ -925,7 +1013,12 @@ export function ExpressCheckoutButton() { if (priceAdjustment && appliedCouponCode) { poyntLineItems.push({ label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + isInCents: true, + returnRaw: true, + }), isPending: false, }); } @@ -949,7 +1042,12 @@ export function ExpressCheckoutButton() { updatedOrder.couponCode = { code: appliedCouponCode, label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + isInCents: true, + returnRaw: true, + }), }; } @@ -978,14 +1076,14 @@ export function ExpressCheckoutButton() { setGoDaddyTotals(value => ({ ...value, shipping: { - currencyCode: 'USD', - value: Number(methods?.[0]?.amount) * 100 || 0, + currencyCode: currencyCode, + value: methods?.[0]?.amountInMinorUnits || 0, }, })); poyntLineItems.push({ label: t.totals.shipping, - amount: (Number(methods?.[0]?.amount) || 0).toString(), + amount: methods?.[0]?.amount || '0', isPending: false, }); @@ -999,7 +1097,7 @@ export function ExpressCheckoutButton() { const shippingLines = convertAddressToShippingLines( e.shippingAddress, { - amount: methods[0]?.amount || '0', + amountInMinorUnits: methods[0]?.amountInMinorUnits || 0, name: methods[0]?.label || t.totals.shipping, } ); @@ -1040,7 +1138,7 @@ export function ExpressCheckoutButton() { setGoDaddyTotals(value => ({ ...value, shipping: { - currencyCode: 'USD', + currencyCode: currencyCode, value: 0, }, })); @@ -1060,13 +1158,18 @@ export function ExpressCheckoutButton() { if (taxesResult?.value) { poyntLineItems.push({ label: t.totals.estimatedTaxes, - amount: (taxesResult.value / 100).toString(), + amount: formatCurrency({ + amount: taxesResult.value, + currencyCode, + isInCents: true, + returnRaw: true, + }), isPending: false, }); setGoDaddyTotals(value => ({ ...value, taxes: { - currencyCode: 'USD', + currencyCode: currencyCode, value: taxesResult.value || 0, }, })); @@ -1094,7 +1197,12 @@ export function ExpressCheckoutButton() { if (priceAdjustment && appliedCouponCode) { poyntLineItems.push({ label: t.totals.discount, - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + isInCents: true, + returnRaw: true, + }), isPending: false, }); } @@ -1119,7 +1227,12 @@ export function ExpressCheckoutButton() { updatedOrder.couponCode = { code: appliedCouponCode, label: appliedCouponCode || 'Discount', - amount: (-priceAdjustment / 100).toString(), + amount: formatCurrency({ + amount: -priceAdjustment, + currencyCode, + isInCents: true, + 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..25443b71 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -57,6 +57,7 @@ import { type PaymentMethodValue, PaymentProvider, } from '@/types'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; // UI config for payment methods (labels will be resolved from translations) const PAYMENT_METHOD_ICONS: Record = { @@ -447,10 +448,11 @@ 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', + isInCents: true, + })} 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..e621ffde 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 { @@ -193,16 +194,16 @@ export function useBuildPaymentRequest(): { [lineItems, skusMap] ); - // 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( + // Extract amounts in minor units for use across payment requests + const subtotalMinorUnits = totals?.subTotal?.value || 0; + const taxMinorUnits = totals?.taxTotal?.value || 0; + const shippingMinorUnits = + 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; + ) || 0; + const discountMinorUnits = totals?.discountTotal?.value || 0; + const totalMinorUnits = totals?.total?.value || 0; const countryCode = useMemo( () => session?.shipping?.originAddress?.countryCode || 'US', @@ -254,52 +255,62 @@ export function useBuildPaymentRequest(): { merchantCapabilities: ['supports3DS'], total: { label: 'Order Total', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(total), + amount: formatCurrency({ + amount: totals?.total?.value || 0, + currencyCode, + isInCents: true, + }), 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, + isInCents: true, + }), type: 'LINE_ITEM', status: 'FINAL', })), { label: 'Subtotal', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(subtotal), + amount: formatCurrency({ + amount: totals?.subTotal?.value || 0, + currencyCode, + isInCents: true, + }), type: 'final', }, { label: 'Tax', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(tax), + amount: formatCurrency({ + amount: totals?.taxTotal?.value || 0, + currencyCode, + isInCents: true, + }), type: 'final', }, { label: 'Shipping', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(shipping), + amount: formatCurrency({ + amount: + order?.shippingLines?.reduce( + (sum, line) => sum + (line?.amount?.value || 0), + 0 + ) || 0, + currencyCode, + isInCents: true, + }), type: 'final', }, { label: 'Discount', - amount: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(-1 * discount), + amount: formatCurrency({ + amount: -1 * (totals?.discountTotal?.value || 0), + currencyCode, + isInCents: true, + }), type: 'final', }, ].filter(item => Number.parseFloat(item.amount) !== 0), @@ -338,10 +349,11 @@ export function useBuildPaymentRequest(): { }, transactionInfo: { totalPriceStatus: 'FINAL', - totalPrice: new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(total), + totalPrice: formatCurrency({ + amount: totals?.total?.value || 0, + currencyCode, + isInCents: true, + }), totalPriceLabel: 'Total', currencyCode, displayItems: [ @@ -353,25 +365,45 @@ export function useBuildPaymentRequest(): { })), { label: 'Subtotal', - price: subtotal, + price: Number.parseFloat(formatCurrency({ + amount: subtotalMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + })), type: 'LINE_ITEM', status: 'FINAL', }, { label: 'Tax', - price: tax, + price: Number.parseFloat(formatCurrency({ + amount: taxMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + })), type: 'LINE_ITEM', status: 'FINAL', }, { label: 'Shipping', - price: shipping, + price: Number.parseFloat(formatCurrency({ + amount: shippingMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + })), type: 'LINE_ITEM', status: 'FINAL', }, { label: 'Discount', - price: -1 * discount, + price: Number.parseFloat(formatCurrency({ + amount: -1 * discountMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + })), type: 'LINE_ITEM', status: 'FINAL', }, @@ -380,30 +412,59 @@ export function useBuildPaymentRequest(): { }; // Create PayPal request with proper breakdown validation - const calculatedTotal = subtotal + tax + shipping - discount; + const calculatedTotalMinorUnits = + subtotalMinorUnits + + taxMinorUnits + + shippingMinorUnits - + discountMinorUnits; const payPalRequest: PayPalRequest = { purchase_units: [ { amount: { currency_code: currencyCode, - value: calculatedTotal.toFixed(2), + value: formatCurrency({ + amount: calculatedTotalMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), breakdown: { item_total: { currency_code: currencyCode, - value: subtotal.toFixed(2), + value: formatCurrency({ + amount: subtotalMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, tax_total: { currency_code: currencyCode, - value: tax.toFixed(2), + value: formatCurrency({ + amount: taxMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, shipping: { currency_code: currencyCode, - value: shipping.toFixed(2), + value: formatCurrency({ + amount: shippingMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, discount: { currency_code: currencyCode, - value: discount.toFixed(2), + value: formatCurrency({ + amount: discountMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, }, }, @@ -411,7 +472,12 @@ export function useBuildPaymentRequest(): { name: lineItem?.name || '', unit_amount: { currency_code: currencyCode, - value: (lineItem?.originalPrice || 0).toFixed(2), + value: formatCurrency({ + amount: lineItem?.originalPrice || 0, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, quantity: (lineItem?.quantity || 1).toString(), })), @@ -500,7 +566,12 @@ export function useBuildPaymentRequest(): { const poyntExpressRequest: PoyntExpressRequest = { total: { label: 'Order Total', - amount: subtotal.toString(), + amount: formatCurrency({ + amount: subtotalMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, lineItems: [ ...(items || []).map(lineItem => { @@ -517,7 +588,12 @@ export function useBuildPaymentRequest(): { const poyntStandardRequest: PoyntStandardRequest = { total: { label: 'Order Total', - amount: total.toString(), + amount: formatCurrency({ + amount: totalMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, lineItems: [ ...(items || []).map(lineItem => { @@ -530,15 +606,30 @@ export function useBuildPaymentRequest(): { }), { label: 'Tax', - amount: tax.toFixed(2), + amount: formatCurrency({ + amount: taxMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, { label: 'Shipping', - amount: shipping.toFixed(2), + amount: formatCurrency({ + amount: shippingMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }), }, { label: 'Discount', - amount: (-1 * discount).toFixed(2), + amount: formatCurrency({ + amount: -1 * discountMinorUnits, + currencyCode, + isInCents: true, + 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..52eade1c 100644 --- a/packages/react/src/components/checkout/shipping/shipping-method.tsx +++ b/packages/react/src/components/checkout/shipping/shipping-method.tsx @@ -20,6 +20,7 @@ import { useGoDaddyContext } from '@/godaddy-provider'; import { eventIds } from '@/tracking/events'; import { TrackingEventType, track } from '@/tracking/track'; import type { ShippingMethod } from '@/types'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; // Helper function to build the shipping payload function buildShippingPayload(method: ShippingMethod) { @@ -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', + isInCents: true, + })} )} @@ -301,10 +303,11 @@ 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', + isInCents: true, + })} )} diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index bfc0aeab..f59d881b 100644 --- a/packages/react/src/components/checkout/tips/tips-form.tsx +++ b/packages/react/src/components/checkout/tips/tips-form.tsx @@ -14,6 +14,7 @@ import { useGoDaddyContext } from '@/godaddy-provider'; import { cn } from '@/lib/utils'; import { eventIds } from '@/tracking/events'; import { TrackingEventType, track } from '@/tracking/track'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; interface TipsFormProps { total: number; @@ -27,15 +28,8 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { const [showCustomTip, setShowCustomTip] = useState(false); const calculateTipAmount = (percentage: number): number => { - 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); + // total is in minor units, so calculate percentage and return in minor units + return Math.round((total * percentage) / 100); }; const handlePercentageSelect = (percentage: number) => { @@ -116,7 +110,11 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { > {percentage}% - {formatCurrency(calculateTipAmount(percentage))} + {formatCurrency({ + amount: calculateTipAmount(percentage), + currencyCode: currencyCode || 'USD', + isInCents: true, + })} ))} @@ -168,19 +166,26 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { {...field} placeholder={t.tips.placeholder} className='h-12' - value={field.value > 0 ? field.value / 100 : ''} + value={field.value > 0 ? formatCurrency({ + amount: field.value, + currencyCode: currencyCode || 'USD', + isInCents: true, + returnRaw: true, + }) : ''} onChange={e => { - // Convert dollars to cents when storing - const tipAmount = Math.round( - Number.parseFloat(e.target.value) * 100 - ); - field.onChange(tipAmount); - - // Only track when user stops typing (on blur) + // User inputs in major units (e.g., $10.50), convert to minor units for storage + const inputValue = Number.parseFloat(e.target.value); + if (!Number.isNaN(inputValue)) { + const tipAmount = Math.round(inputValue * 100); + field.onChange(tipAmount); + } else { + field.onChange(0); + } }} onBlur={e => { - const tipAmount = - Math.round(Number.parseFloat(e.target.value) * 100) || 0; + // User inputs in major units (e.g., $10.50), convert to minor units for storage + const inputValue = Number.parseFloat(e.target.value); + const tipAmount = !Number.isNaN(inputValue) ? Math.round(inputValue * 100) : 0; // Track custom tip amount entry track({ eventId: eventIds.enterCustomTip, diff --git a/packages/react/src/components/checkout/totals/totals.tsx b/packages/react/src/components/checkout/totals/totals.tsx index ca4c4751..a9f1d222 100644 --- a/packages/react/src/components/checkout/totals/totals.tsx +++ b/packages/react/src/components/checkout/totals/totals.tsx @@ -1,6 +1,7 @@ import { DiscountStandalone } from '@/components/checkout/discount/discount-standalone'; import { TotalLineItemSkeleton } from '@/components/checkout/totals/totals-skeleton'; import { useGoDaddyContext } from '@/godaddy-provider'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; export interface DraftOrderTotalsProps { subtotal?: number; @@ -40,10 +41,11 @@ function TotalLineItem({ ) : null} - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(value)} + {formatCurrency({ + amount: value, + currencyCode, + isInCents: true, + })} ); @@ -143,10 +145,11 @@ export function DraftOrderTotals({ {currencyCode}{' '} - {new Intl.NumberFormat('en-us', { - style: 'currency', - currency: currencyCode, - }).format(total)} + {formatCurrency({ + amount: total, + currencyCode, + isInCents: true, + })} diff --git a/packages/react/src/components/checkout/utils/checkout-transformers.ts b/packages/react/src/components/checkout/utils/checkout-transformers.ts index cb107ad0..ed98ef3b 100644 --- a/packages/react/src/components/checkout/utils/checkout-transformers.ts +++ b/packages/react/src/components/checkout/utils/checkout-transformers.ts @@ -148,14 +148,12 @@ export function mapSkusToItemsDisplay( quantity: orderItem.quantity || 0, originalPrice: (orderItem.totals?.subTotal?.value ?? 0) / - (orderItem.quantity || 0) / - 100, + (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?.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); +} From 8094ac3602a4e8c47ab3180d97c2c361378e59e2 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Mon, 10 Nov 2025 13:21:25 -0600 Subject: [PATCH 2/8] lint fix --- .../checkout/form/checkout-form.tsx | 11 ++-- .../checkout/line-items/line-items.tsx | 2 +- .../checkout/payment/payment-form.tsx | 2 +- .../utils/use-build-payment-request.ts | 56 +++++++++++-------- .../checkout/shipping/shipping-method.tsx | 5 +- .../components/checkout/tips/tips-form.tsx | 22 +++++--- .../src/components/checkout/totals/totals.tsx | 2 +- .../checkout/utils/checkout-transformers.ts | 11 ++-- 8 files changed, 63 insertions(+), 48 deletions(-) diff --git a/packages/react/src/components/checkout/form/checkout-form.tsx b/packages/react/src/components/checkout/form/checkout-form.tsx index 494b213d..67bc9754 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, @@ -50,7 +51,6 @@ import { TrackingEventType, track } from '@/tracking/track'; import { CheckoutType, PaymentMethodType } from '@/types'; import { FreePaymentForm } from '../payment/free-payment-form'; import { CustomFormProvider } from './custom-form-provider'; -import { formatCurrency } from '@/components/checkout/utils/format-currency'; const deliveryMethodToGridArea: Record = { SHIP: 'shipping', @@ -109,10 +109,11 @@ export function CheckoutForm({ // Order summary calculations - keep all values in minor units const subtotal = totals?.subTotal?.value || 0; const orderDiscount = totals?.discountTotal?.value || 0; - const shipping = order?.shippingLines?.reduce( - (sum, line) => sum + (line?.amount?.value || 0), - 0 - ) || 0; + const shipping = + order?.shippingLines?.reduce( + (sum, line) => sum + (line?.amount?.value || 0), + 0 + ) || 0; const taxTotal = totals?.taxTotal?.value || 0; const orderTotal = totals?.total?.value || 0; const tipTotal = tipAmount || 0; 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 35fff0b6..101aef01 100644 --- a/packages/react/src/components/checkout/line-items/line-items.tsx +++ b/packages/react/src/components/checkout/line-items/line-items.tsx @@ -1,9 +1,9 @@ // import { Badge } from "@/components/ui/badge"; import { Image } from 'lucide-react'; +import { formatCurrency } from '@/components/checkout/utils/format-currency'; import { useGoDaddyContext } from '@/godaddy-provider'; import type { SKUProduct } from '@/types'; -import { formatCurrency } from '@/components/checkout/utils/format-currency'; export interface Note { content: string | null; diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 25443b71..1e64bb02 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, @@ -57,7 +58,6 @@ import { type PaymentMethodValue, PaymentProvider, } from '@/types'; -import { formatCurrency } from '@/components/checkout/utils/format-currency'; // UI config for payment methods (labels will be resolved from translations) const PAYMENT_METHOD_ICONS: Record = { 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 e621ffde..51aaf375 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 @@ -365,45 +365,53 @@ export function useBuildPaymentRequest(): { })), { label: 'Subtotal', - price: Number.parseFloat(formatCurrency({ - amount: subtotalMinorUnits, - currencyCode, - isInCents: true, - returnRaw: true, - })), + price: Number.parseFloat( + formatCurrency({ + amount: subtotalMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }) + ), type: 'LINE_ITEM', status: 'FINAL', }, { label: 'Tax', - price: Number.parseFloat(formatCurrency({ - amount: taxMinorUnits, - currencyCode, - isInCents: true, - returnRaw: true, - })), + price: Number.parseFloat( + formatCurrency({ + amount: taxMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }) + ), type: 'LINE_ITEM', status: 'FINAL', }, { label: 'Shipping', - price: Number.parseFloat(formatCurrency({ - amount: shippingMinorUnits, - currencyCode, - isInCents: true, - returnRaw: true, - })), + price: Number.parseFloat( + formatCurrency({ + amount: shippingMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }) + ), type: 'LINE_ITEM', status: 'FINAL', }, { label: 'Discount', - price: Number.parseFloat(formatCurrency({ - amount: -1 * discountMinorUnits, - currencyCode, - isInCents: true, - returnRaw: true, - })), + price: Number.parseFloat( + formatCurrency({ + amount: -1 * discountMinorUnits, + currencyCode, + isInCents: true, + returnRaw: true, + }) + ), type: 'LINE_ITEM', status: 'FINAL', }, diff --git a/packages/react/src/components/checkout/shipping/shipping-method.tsx b/packages/react/src/components/checkout/shipping/shipping-method.tsx index 52eade1c..65e35a0a 100644 --- a/packages/react/src/components/checkout/shipping/shipping-method.tsx +++ b/packages/react/src/components/checkout/shipping/shipping-method.tsx @@ -14,13 +14,13 @@ 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'; import { eventIds } from '@/tracking/events'; import { TrackingEventType, track } from '@/tracking/track'; import type { ShippingMethod } from '@/types'; -import { formatCurrency } from '@/components/checkout/utils/format-currency'; // Helper function to build the shipping payload function buildShippingPayload(method: ShippingMethod) { @@ -262,7 +262,8 @@ export function ShippingMethodForm() { {formatCurrency({ amount: shippingMethods[0]?.cost?.value || 0, - currencyCode: shippingMethods[0]?.cost?.currencyCode || 'USD', + currencyCode: + shippingMethods[0]?.cost?.currencyCode || 'USD', isInCents: true, })} diff --git a/packages/react/src/components/checkout/tips/tips-form.tsx b/packages/react/src/components/checkout/tips/tips-form.tsx index f59d881b..7451436d 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, @@ -14,7 +15,6 @@ import { useGoDaddyContext } from '@/godaddy-provider'; import { cn } from '@/lib/utils'; import { eventIds } from '@/tracking/events'; import { TrackingEventType, track } from '@/tracking/track'; -import { formatCurrency } from '@/components/checkout/utils/format-currency'; interface TipsFormProps { total: number; @@ -166,12 +166,16 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { {...field} placeholder={t.tips.placeholder} className='h-12' - value={field.value > 0 ? formatCurrency({ - amount: field.value, - currencyCode: currencyCode || 'USD', - isInCents: true, - returnRaw: true, - }) : ''} + value={ + field.value > 0 + ? formatCurrency({ + amount: field.value, + currencyCode: currencyCode || 'USD', + isInCents: true, + returnRaw: true, + }) + : '' + } onChange={e => { // User inputs in major units (e.g., $10.50), convert to minor units for storage const inputValue = Number.parseFloat(e.target.value); @@ -185,7 +189,9 @@ export function TipsForm({ total, currencyCode }: TipsFormProps) { onBlur={e => { // User inputs in major units (e.g., $10.50), convert to minor units for storage const inputValue = Number.parseFloat(e.target.value); - const tipAmount = !Number.isNaN(inputValue) ? Math.round(inputValue * 100) : 0; + const tipAmount = !Number.isNaN(inputValue) + ? Math.round(inputValue * 100) + : 0; // Track custom tip amount entry track({ eventId: eventIds.enterCustomTip, diff --git a/packages/react/src/components/checkout/totals/totals.tsx b/packages/react/src/components/checkout/totals/totals.tsx index a9f1d222..aa13d379 100644 --- a/packages/react/src/components/checkout/totals/totals.tsx +++ b/packages/react/src/components/checkout/totals/totals.tsx @@ -1,7 +1,7 @@ import { DiscountStandalone } from '@/components/checkout/discount/discount-standalone'; import { TotalLineItemSkeleton } from '@/components/checkout/totals/totals-skeleton'; -import { useGoDaddyContext } from '@/godaddy-provider'; import { formatCurrency } from '@/components/checkout/utils/format-currency'; +import { useGoDaddyContext } from '@/godaddy-provider'; export interface DraftOrderTotalsProps { subtotal?: number; diff --git a/packages/react/src/components/checkout/utils/checkout-transformers.ts b/packages/react/src/components/checkout/utils/checkout-transformers.ts index ed98ef3b..02b92f1d 100644 --- a/packages/react/src/components/checkout/utils/checkout-transformers.ts +++ b/packages/react/src/components/checkout/utils/checkout-transformers.ts @@ -147,13 +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), + (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)), + (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 => From 78633c0a1b9330a623862b77a54f83a8ab0cf0e6 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Mon, 10 Nov 2025 13:37:07 -0600 Subject: [PATCH 3/8] add isInCents props for totals and line items --- .../src/components/checkout/form/checkout-form.tsx | 4 ++++ .../components/checkout/line-items/line-items.tsx | 4 +++- .../components/checkout/payment/payment-form.tsx | 2 ++ .../react/src/components/checkout/totals/totals.tsx | 13 +++++++++++-- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/checkout/form/checkout-form.tsx b/packages/react/src/components/checkout/form/checkout-form.tsx index 67bc9754..ee7cd7cb 100644 --- a/packages/react/src/components/checkout/form/checkout-form.tsx +++ b/packages/react/src/components/checkout/form/checkout-form.tsx @@ -441,9 +441,11 @@ export function CheckoutForm({ & { export interface DraftOrderLineItemsProps { items: Product[]; currencyCode?: string; + isInCents?: boolean; } export function DraftOrderLineItems({ items, currencyCode = 'USD', + isInCents = false, }: DraftOrderLineItemsProps) { const { t } = useGoDaddyContext(); @@ -133,7 +135,7 @@ export function DraftOrderLineItems({ {formatCurrency({ amount: item.originalPrice * item.quantity, currencyCode, - isInCents: true, + isInCents, })} diff --git a/packages/react/src/components/checkout/payment/payment-form.tsx b/packages/react/src/components/checkout/payment/payment-form.tsx index 1e64bb02..417f2393 100644 --- a/packages/react/src/components/checkout/payment/payment-form.tsx +++ b/packages/react/src/components/checkout/payment/payment-form.tsx @@ -461,6 +461,7 @@ export function PaymentForm( @@ -468,6 +469,7 @@ export function PaymentForm(
@@ -44,7 +47,7 @@ function TotalLineItem({ {formatCurrency({ amount: value, currencyCode, - isInCents: true, + isInCents, })}
@@ -66,6 +69,7 @@ export function DraftOrderTotals({ isShippingLoading = false, isDiscountLoading = false, enableShipping = true, + isInCents = false, }: DraftOrderTotalsProps) { const { t } = useGoDaddyContext(); const handleDiscountsChange = (discounts: string[]) => { @@ -84,6 +88,7 @@ export function DraftOrderTotals({ : t.totals.noItems } value={subtotal} + isInCents={isInCents} /> {discount > 0 ? ( isDiscountLoading ? ( @@ -93,6 +98,7 @@ export function DraftOrderTotals({ currencyCode={currencyCode} title={t.totals.discount} value={-discount || 0} + isInCents={isInCents} /> ) ) : null} @@ -104,6 +110,7 @@ export function DraftOrderTotals({ currencyCode={currencyCode} title={t.totals.shipping} value={shipping || 0} + isInCents={isInCents} /> ))} {tip ? ( @@ -111,6 +118,7 @@ export function DraftOrderTotals({ currencyCode={currencyCode} title={t.totals.tip} value={tip || 0} + isInCents={isInCents} /> ) : null} {enableTaxes && @@ -121,6 +129,7 @@ export function DraftOrderTotals({ currencyCode={currencyCode} title={t.totals.estimatedTaxes} value={taxes || 0} + isInCents={isInCents} /> ))} @@ -148,7 +157,7 @@ export function DraftOrderTotals({ {formatCurrency({ amount: total, currencyCode, - isInCents: true, + isInCents, })} From 5288818055ecfa16d6a59c67ebe7b3f1e31250c8 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Wed, 12 Nov 2025 12:38:23 -0600 Subject: [PATCH 4/8] add changeset --- .changeset/stupid-lemons-smash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stupid-lemons-smash.md diff --git a/.changeset/stupid-lemons-smash.md b/.changeset/stupid-lemons-smash.md new file mode 100644 index 00000000..37e4db7d --- /dev/null +++ b/.changeset/stupid-lemons-smash.md @@ -0,0 +1,5 @@ +--- +"@godaddy/react": patch +--- + +Refactor currency formatting to respect currency precision From 3003350c3592930c38cca66b35bfe507d18e38f0 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Wed, 12 Nov 2025 12:39:59 -0600 Subject: [PATCH 5/8] lint fixes --- .../react/src/components/checkout/checkout.tsx | 2 +- .../checkout/delivery/delivery-method.tsx | 9 +++++---- .../checkout/form/checkout-form-container.tsx | 14 +++++++------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/react/src/components/checkout/checkout.tsx b/packages/react/src/components/checkout/checkout.tsx index cdc232aa..d44a3add 100644 --- a/packages/react/src/components/checkout/checkout.tsx +++ b/packages/react/src/components/checkout/checkout.tsx @@ -227,7 +227,7 @@ export function Checkout(props: CheckoutProps) { const [checkoutErrors, setCheckoutErrors] = React.useState< string[] | undefined >(undefined); - const { t } = useGoDaddyContext() + const { t } = useGoDaddyContext(); const { session, jwt, isLoading: isLoadingJWT } = useCheckoutSession(props); diff --git a/packages/react/src/components/checkout/delivery/delivery-method.tsx b/packages/react/src/components/checkout/delivery/delivery-method.tsx index 5aa5aaa7..0861708e 100644 --- a/packages/react/src/components/checkout/delivery/delivery-method.tsx +++ b/packages/react/src/components/checkout/delivery/delivery-method.tsx @@ -82,13 +82,14 @@ export function DeliveryMethodForm() { useEffect(() => { const currentValue = form.getValues('deliveryMethod'); const isCurrentValueValid = availableMethods.some( - (method) => method.id === currentValue + method => method.id === currentValue ); if (!currentValue || !isCurrentValueValid) { - const defaultMethod = availableMethods.length === 1 - ? availableMethods[0].id - : DeliveryMethods.SHIP; + const defaultMethod = + availableMethods.length === 1 + ? availableMethods[0].id + : DeliveryMethods.SHIP; form.setValue('deliveryMethod', defaultMethod); } }, [availableMethods, form]); diff --git a/packages/react/src/components/checkout/form/checkout-form-container.tsx b/packages/react/src/components/checkout/form/checkout-form-container.tsx index 055ae17a..bc17109d 100644 --- a/packages/react/src/components/checkout/form/checkout-form-container.tsx +++ b/packages/react/src/components/checkout/form/checkout-form-container.tsx @@ -51,13 +51,13 @@ export function CheckoutFormContainer({ [order, props.defaultValues, session?.shipping?.originAddress?.countryCode] ); - // if (!isConfirmingCheckout && !draftOrderQuery.isLoading && !order) { - // const returnUrl = session?.returnUrl; - // if (returnUrl) { - // window.location.href = returnUrl; - // return null; - // } - // } + if (!isConfirmingCheckout && !draftOrderQuery.isLoading && !order) { + const returnUrl = session?.returnUrl; + if (returnUrl) { + window.location.href = returnUrl; + return null; + } + } if (draftOrderQuery.isLoading || isLoadingJWT) { return ; From 84e74f527b67d509070a1e5e36ac94b4a8031966 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Wed, 12 Nov 2025 13:26:54 -0600 Subject: [PATCH 6/8] fix skus query and public clientid --- examples/nextjs/app/page.tsx | 2 +- packages/react/src/lib/godaddy/queries.ts | 1 - packages/react/src/server.ts | 5 ++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx index 54e0d1ad..9ae69eb8 100644 --- a/examples/nextjs/app/page.tsx +++ b/examples/nextjs/app/page.tsx @@ -90,7 +90,7 @@ export default async function Home() { }, { auth: { - clientId: process.env.GODADDY_CLIENT_ID || '', + clientId: process.env.NEXT_PUBLIC_GODADDY_CLIENT_ID || '', clientSecret: process.env.GODADDY_CLIENT_SECRET || '', }, } diff --git a/packages/react/src/lib/godaddy/queries.ts b/packages/react/src/lib/godaddy/queries.ts index a884ade8..6dd5a2cd 100644 --- a/packages/react/src/lib/godaddy/queries.ts +++ b/packages/react/src/lib/godaddy/queries.ts @@ -472,7 +472,6 @@ export const DraftOrderSkusQuery = graphql(` name label } - mediaUrls } } pageInfo { diff --git a/packages/react/src/server.ts b/packages/react/src/server.ts index fc9ee3c9..92cee345 100644 --- a/packages/react/src/server.ts +++ b/packages/react/src/server.ts @@ -3,7 +3,7 @@ import * as GoDaddy from '@/lib/godaddy/godaddy'; import { CreateCheckoutSessionInputWithKebabCase } from '@/lib/godaddy/godaddy'; import { getEnvVar } from '@/lib/utils'; -import type { CheckoutSessionInput, CheckoutSessionOptions } from '@/types'; +import type { CheckoutSessionOptions } from '@/types'; let accessToken: string | undefined; let accessTokenExpiresAt: number | undefined; @@ -81,10 +81,9 @@ async function getAccessToken({ ); } - const result = (await response.json()) as { + return (await response.json()) as { access_token: string; scope: string; expires_in: number; }; - return result; } From 319627f5ddf9b8045a98b76d1a1120f1a59f3d40 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 13 Nov 2025 09:30:10 -0600 Subject: [PATCH 7/8] amp fixes and precision 2 fallback --- .../checkout/utils/format-currency.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/react/src/components/checkout/utils/format-currency.ts b/packages/react/src/components/checkout/utils/format-currency.ts index 1b71c3e0..36fbf69d 100644 --- a/packages/react/src/components/checkout/utils/format-currency.ts +++ b/packages/react/src/components/checkout/utils/format-currency.ts @@ -28,6 +28,13 @@ export const currencyConfigs: Record< TRY: { symbol: '₺', precision: 2 }, ZAR: { symbol: 'R', precision: 2 }, CNY: { symbol: '¥', precision: 2 }, + JPY: { symbol: '¥', precision: 0 }, + KRW: { symbol: '₩', precision: 0 }, + TWD: { symbol: 'NT$', precision: 0 }, + KWD: { symbol: 'د.ك', precision: 3 }, + BHD: { symbol: '.د.ب', precision: 3 }, + JOD: { symbol: 'د.ا', precision: 3 }, + OMR: { symbol: 'ر.ع.', precision: 3 }, }; export interface FormatCurrencyOptions { @@ -65,13 +72,9 @@ export function formatCurrency({ isInCents = true, returnRaw = false, }: FormatCurrencyOptions): string { - const config = currencyConfigs[currencyCode]; + const config = currencyConfigs[currencyCode] || {}; - if (!config) { - return amount.toString(); - } - - const { precision } = config; + const { precision = 2 } = config; if (!isInCents) { // Convert major units to minor units and return as string @@ -81,10 +84,13 @@ export function formatCurrency({ // Format value already in minor units const value = amount / Math.pow(10, precision); - return new Intl.NumberFormat(locale, { + const nfLocale = returnRaw ? 'en-US' : locale; + + return new Intl.NumberFormat(nfLocale, { style: returnRaw ? 'decimal' : 'currency', currency: returnRaw ? undefined : currencyCode, minimumFractionDigits: precision, maximumFractionDigits: precision, + useGrouping: returnRaw ? false : undefined, }).format(value); } From cf9ded4e799f8cc6158d99b5aa68ded9ff8f2d39 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Thu, 13 Nov 2025 09:56:38 -0600 Subject: [PATCH 8/8] fix line item currency formatting for gdp requests --- .../payment/utils/use-build-payment-request.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 51aaf375..ac596bf2 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 @@ -585,9 +585,12 @@ export function useBuildPaymentRequest(): { ...(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, + isInCents: true, + returnRaw: true, + }), }; }), ], @@ -607,9 +610,12 @@ export function useBuildPaymentRequest(): { ...(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, + isInCents: true, + returnRaw: true, + }), }; }), {