From 4f96f5336e373624e56513124f0f730094217017 Mon Sep 17 00:00:00 2001 From: bc-ania Date: Tue, 14 Jul 2026 13:43:05 +0200 Subject: [PATCH] feat(payment): PI-5431 [SPIKE] [FE] Surcharging (Bluesnap, Adyen) --- packages/core/src/app/cart/withRedeemable.tsx | 3 +++ .../app/coupon/NewOrderSummarySubtotals.tsx | 24 ++++++++++++++++- .../src/app/order/OrderSummarySubtotals.tsx | 24 ++++++++++++++++- packages/core/src/app/payment/Payment.tsx | 27 +++++++++++++++++-- packages/locale/src/translations/en.json | 1 + 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/packages/core/src/app/cart/withRedeemable.tsx b/packages/core/src/app/cart/withRedeemable.tsx index 0766400864..74acd925af 100644 --- a/packages/core/src/app/cart/withRedeemable.tsx +++ b/packages/core/src/app/cart/withRedeemable.tsx @@ -43,6 +43,9 @@ export default function withRedeemable( showHeader={showHeader} storeCreditAmount={storeCreditAmount} storeCurrency={storeCurrency} + // Surcharging: assumes the BE Fees API recomputes outstandingBalance to include + // the surcharge. Until that endpoint is implemented on the BE, no surcharge is + // applied, so this is just the normal total. total={checkout.outstandingBalance} /> ); diff --git a/packages/core/src/app/coupon/NewOrderSummarySubtotals.tsx b/packages/core/src/app/coupon/NewOrderSummarySubtotals.tsx index 61e03ca026..a9e09e02dc 100644 --- a/packages/core/src/app/coupon/NewOrderSummarySubtotals.tsx +++ b/packages/core/src/app/coupon/NewOrderSummarySubtotals.tsx @@ -9,6 +9,13 @@ import { CollapseCSSTransition } from '@bigcommerce/checkout/ui'; import { isOrderFee, OrderSummaryDiscount, OrderSummaryPrice } from '../order'; import { AppliedGiftCertificates, CouponForm, Discounts } from './components'; + +// Must match SURCHARGE_FEE_NAME in the SDK surcharge handler. +const SURCHARGE_FEE_NAME = 'corporate_card_surcharge'; + +const isSurchargeFee = (fee: Fee | OrderFee): boolean => + 'name' in fee && fee.name === SURCHARGE_FEE_NAME; + import { useMultiCoupon } from './useMultiCoupon'; import { getRedeemableLabelId } from './utils'; @@ -44,6 +51,12 @@ const NewOrderSummarySubtotals: FunctionComponent = ({ const [isCouponFormVisible, setIsCouponFormVisible] = useState(!isCouponFormCollapsed); const couponFormRef = useRef(null); + // Shown as its own labelled row; other fees stay generic. + // NOTE: the surcharge fee comes from the BE Fees API (Checkout.fees). Until that endpoint + // is implemented on the BE, no surcharge fee is present, so this row simply won't render. + const surchargeFee = (fees ?? []).find(isSurchargeFee); + const otherFees = (fees ?? []).filter((fee) => !isSurchargeFee(fee)) as typeof fees; + const toggleCouponForm = () => { setIsCouponFormVisible((prevState) => !prevState); }; @@ -99,7 +112,16 @@ const NewOrderSummarySubtotals: FunctionComponent = ({ /> )} - {fees?.map((fee, index) => ( + {/* Surcharge placeholder — shown once applied in-flight. */} + {!!surchargeFee && ( + } + testId="cart-surcharge" + /> + )} + + {otherFees?.map((fee, index) => ( + 'name' in fee && fee.name === SURCHARGE_FEE_NAME; + export interface OrderSummarySubtotalsProps { coupons: Coupon[]; giftCertificates?: GiftCertificate[]; @@ -46,6 +52,13 @@ const OrderSummarySubtotals: FunctionComponent = ({ onRemovedGiftCertificate, onRemovedCoupon, }) => { + // Surcharging: render the surcharge as its own labelled row, alongside shipping / + // handling; keep other fees in the generic loop below. + // NOTE: the surcharge fee comes from the BE Fees API (Checkout.fees). Until that endpoint + // is implemented on the BE, no surcharge fee is present, so this row simply won't render. + const surchargeFee = (fees ?? []).find(isSurchargeFee); + const otherFees = (fees ?? []).filter((fee) => !isSurchargeFee(fee)) as typeof fees; + return ( <> = ({ /> )} - {fees?.map((fee, index) => ( + {/* Surcharge placeholder — shown once applied in-flight. */} + {!!surchargeFee && ( + } + testId="cart-surcharge" + /> + )} + + {otherFees?.map((fee, index) => ( void>(); + const lastSurchargeTotalRef = useRef(0); const validationSchemasRef = useRef({}); const lastFormValuesRef = useRef(null); // Set by the themeV2 billing form. Awaited before submitOrder so the order @@ -637,13 +641,28 @@ const Payment = ( } }; - const handleCartTotalChange = async (): Promise => { + const handleCartTotalChange = async (state?: CheckoutSelectors): Promise => { const isReady = isReadyRef.current; if (!isReady) { return; } + // If the total change was caused by applying the + // surcharge fee, skip reloading payment methods. Reloading re-initialises the hosted + // (iframe) card fields and clears the card the shopper just entered. A real cart + // change (coupon/shipping) leaves the surcharge total unchanged and reloads as usual. + const checkout = state?.data.getCheckout(); + const surchargeTotal = (checkout?.fees ?? []) + .filter((fee) => fee.name === SURCHARGE_FEE_NAME) + .reduce((sum, fee) => sum + fee.cost, 0); + + if (surchargeTotal !== lastSurchargeTotalRef.current) { + lastSurchargeTotalRef.current = surchargeTotal; + + return; + } + setState((prevState) => ({ ...prevState, isReady: false })); await loadPaymentMethodsOrThrow(); @@ -724,7 +743,11 @@ const Payment = ( } grandTotalChangeUnsubscribe.current = checkoutServiceSubscribe( - () => handleCartTotalChange(), + // Pass state so we can detect a + // surcharge-driven total change and skip the payment-methods reload. + (state) => { + void handleCartTotalChange(state); + }, ({ data }) => data.getCheckout()?.grandTotal, ({ data }) => data.getCheckout()?.outstandingBalance, ); diff --git a/packages/locale/src/translations/en.json b/packages/locale/src/translations/en.json index d7f7b11dc5..ca4f1ba524 100644 --- a/packages/locale/src/translations/en.json +++ b/packages/locale/src/translations/en.json @@ -71,6 +71,7 @@ "free_text": "Free", "gift_certificate_text": "Gift Certificate", "handling_text": "Handling", + "surcharge_text": "Surcharge", "item": "item", "items": "items", "item_count_text": "{count, plural, one{1 Item} other{# Items} }",