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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/src/app/cart/withRedeemable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
);
Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/app/coupon/NewOrderSummarySubtotals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -44,6 +51,12 @@ const NewOrderSummarySubtotals: FunctionComponent<MultiCouponProps> = ({
const [isCouponFormVisible, setIsCouponFormVisible] = useState(!isCouponFormCollapsed);
const couponFormRef = useRef<HTMLDivElement>(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);
};
Expand Down Expand Up @@ -99,7 +112,16 @@ const NewOrderSummarySubtotals: FunctionComponent<MultiCouponProps> = ({
/>
)}

{fees?.map((fee, index) => (
{/* Surcharge placeholder — shown once applied in-flight. */}
{!!surchargeFee && (
<OrderSummaryPrice
amount={surchargeFee.cost}
label={<TranslatedString id="cart.surcharge_text" />}
testId="cart-surcharge"
/>
)}

{otherFees?.map((fee, index) => (
<OrderSummaryPrice
amount={fee.cost}
key={index}
Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/app/order/OrderSummarySubtotals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import isOrderFee from './isOrderFee';
import OrderSummaryDiscount from './OrderSummaryDiscount';
import OrderSummaryPrice from './OrderSummaryPrice';

// SMust 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;

export interface OrderSummarySubtotalsProps {
coupons: Coupon[];
giftCertificates?: GiftCertificate[];
Expand Down Expand Up @@ -46,6 +52,13 @@ const OrderSummarySubtotals: FunctionComponent<OrderSummarySubtotalsProps> = ({
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 (
<>
<OrderSummaryPrice
Expand Down Expand Up @@ -110,7 +123,16 @@ const OrderSummarySubtotals: FunctionComponent<OrderSummarySubtotalsProps> = ({
/>
)}

{fees?.map((fee, index) => (
{/* Surcharge placeholder — shown once applied in-flight. */}
{!!surchargeFee && (
<OrderSummaryPrice
amount={surchargeFee.cost}
label={<TranslatedString id="cart.surcharge_text" />}
testId="cart-surcharge"
/>
)}

{otherFees?.map((fee, index) => (
<OrderSummaryPrice
amount={fee.cost}
key={index}
Expand Down
27 changes: 25 additions & 2 deletions packages/core/src/app/payment/Payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ import PaymentForm from './PaymentForm';
import { getUniquePaymentMethodId, PaymentMethodProviderType } from './paymentMethod';
import { getFilteredPaymentMethodsWithDefault } from './paymentMethodFilters';

// Must match SURCHARGE_FEE_NAME in the SDK surcharge handler.
const SURCHARGE_FEE_NAME = 'corporate_card_surcharge';

export interface PaymentProps {
capabilities: Capabilities;
errorLogger: ErrorLogger;
Expand Down Expand Up @@ -160,6 +163,7 @@ const Payment = (

const isReadyRef = useRef(state.isReady);
const grandTotalChangeUnsubscribe = useRef<() => void>();
const lastSurchargeTotalRef = useRef(0);
const validationSchemasRef = useRef<validationSchemas>({});
const lastFormValuesRef = useRef<PaymentFormValues | null>(null);
// Set by the themeV2 billing form. Awaited before submitOrder so the order
Expand Down Expand Up @@ -637,13 +641,28 @@ const Payment = (
}
};

const handleCartTotalChange = async (): Promise<void> => {
const handleCartTotalChange = async (state?: CheckoutSelectors): Promise<void> => {
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reload skipped on cart changes

High Severity

handleCartTotalChange skips loadPaymentMethods whenever the summed corporate_card_surcharge fee amount differs from the last seen value. That also happens when coupons, shipping, or other cart updates change the surcharge while the total moves, so payment methods may not reload and hosted card flows can keep a stale payable amount.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f96f53. Configure here.


setState((prevState) => ({ ...prevState, isReady: false }));

await loadPaymentMethodsOrThrow();
Expand Down Expand Up @@ -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,
);
Expand Down
1 change: 1 addition & 0 deletions packages/locale/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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} }",
Expand Down