diff --git a/packages/core/src/app/coupon/components/CouponForm.tsx b/packages/core/src/app/coupon/components/CouponForm.tsx index 2a84245ad2..eb23810ed0 100644 --- a/packages/core/src/app/coupon/components/CouponForm.tsx +++ b/packages/core/src/app/coupon/components/CouponForm.tsx @@ -11,26 +11,40 @@ import { useMultiCoupon } from '../useMultiCoupon'; import { AppliedCouponsOrGiftCertificates } from './AppliedCouponsOrGiftCertificates'; export const CouponForm: FunctionComponent = () => { - const [applyCouponError, setApplyCouponError] = useState(null); const [code, setCode] = useState(''); const { themeV2 } = useThemeContext(); const { language } = useLocale(); - const { applyCouponOrGiftCertificate } = useMultiCoupon(); + const { + applyCouponOrGiftCertificate, + couponError, + setCouponError, + shouldDisableCouponForm, + } = useMultiCoupon(); const handleTextInputChange = (event: React.ChangeEvent) => { setCode(event.currentTarget.value.trim()); }; + const clearErrorOnClick = () => { + if (couponError) { + setCouponError(null); + } + }; + const submitForm = async () => { + if (!code) { + return; + } + try { await applyCouponOrGiftCertificate(code); setCode(''); } catch (error) { - // TODO: Handle different error types accordingly - // eslint-disable-next-line no-console - console.log(error); + if (error instanceof Error) { + setCouponError(error.message); + } } }; @@ -40,7 +54,9 @@ export const CouponForm: FunctionComponent = () => { { className={classNames('coupon-button', { 'body-bold': themeV2, })} + disabled={shouldDisableCouponForm} id="applyRedeemableButton" onClick={submitForm} testId="redeemableEntry-submit" @@ -59,11 +76,11 @@ export const CouponForm: FunctionComponent = () => {
- {Boolean(applyCouponError) && + {Boolean(couponError) &&
    - {applyCouponError} - setApplyCouponError(null)}> + {couponError} + setCouponError(null)}>
} diff --git a/packages/core/src/app/coupon/useMultiCoupon.test.ts b/packages/core/src/app/coupon/useMultiCoupon.test.ts index 09678e6458..03b1521ad2 100644 --- a/packages/core/src/app/coupon/useMultiCoupon.test.ts +++ b/packages/core/src/app/coupon/useMultiCoupon.test.ts @@ -28,6 +28,10 @@ describe('useMultiCoupon', () => { getCoupons: jest.fn(), getGiftCertificates: jest.fn(), }, + statuses: { + isSubmittingOrder: jest.fn(), + isPending: jest.fn(), + }, }; beforeEach(() => { @@ -39,6 +43,8 @@ describe('useMultiCoupon', () => { checkoutState.data.getConfig.mockReturnValue(getStoreConfig()); checkoutState.data.getCoupons.mockReturnValue([]); checkoutState.data.getGiftCertificates.mockReturnValue([]); + checkoutState.statuses.isSubmittingOrder.mockReturnValue(false); + checkoutState.statuses.isPending.mockReturnValue(false); }); afterEach(() => { @@ -51,8 +57,11 @@ describe('useMultiCoupon', () => { expect(result.current.appliedCoupons).toEqual([]); expect(result.current.appliedGiftCertificates).toEqual([]); expect(result.current.applyCouponOrGiftCertificate).toBeInstanceOf(Function); + expect(result.current.couponError).toBe(null); expect(result.current.removeCoupon).toBeInstanceOf(Function); expect(result.current.removeGiftCertificate).toBeInstanceOf(Function); + expect(result.current.setCouponError).toBeInstanceOf(Function); + expect(result.current.shouldDisableCouponForm).toBe(false); expect(result.current.isCouponCodeCollapsed).toBe(true); }); @@ -243,5 +252,77 @@ describe('useMultiCoupon', () => { expect(removeGiftCertificate).toHaveBeenCalledTimes(1); }); }); + + describe('couponError', () => { + it('initializes with null error', () => { + const { result } = renderHook(() => useMultiCoupon()); + + expect(result.current.couponError).toBe(null); + }); + + it('sets error when setCouponError is called', () => { + const { result } = renderHook(() => useMultiCoupon()); + + act(() => { + result.current.setCouponError('Test error message'); + }); + + expect(result.current.couponError).toBe('Test error message'); + }); + + it('clears error when setCouponError is called with null', () => { + const { result } = renderHook(() => useMultiCoupon()); + + act(() => { + result.current.setCouponError('Test error message'); + }); + + expect(result.current.couponError).toBe('Test error message'); + + act(() => { + result.current.setCouponError(null); + }); + + expect(result.current.couponError).toBe(null); + }); + }); + + describe('shouldDisableCouponForm', () => { + it('returns false when order is not being submitted and not pending', () => { + checkoutState.statuses.isSubmittingOrder.mockReturnValue(false); + checkoutState.statuses.isPending.mockReturnValue(false); + + const { result } = renderHook(() => useMultiCoupon()); + + expect(result.current.shouldDisableCouponForm).toBe(false); + }); + + it('returns true when order is being submitted', () => { + checkoutState.statuses.isSubmittingOrder.mockReturnValue(true); + checkoutState.statuses.isPending.mockReturnValue(false); + + const { result } = renderHook(() => useMultiCoupon()); + + expect(result.current.shouldDisableCouponForm).toBe(true); + }); + + it('returns true when order is pending', () => { + checkoutState.statuses.isSubmittingOrder.mockReturnValue(false); + checkoutState.statuses.isPending.mockReturnValue(true); + + const { result } = renderHook(() => useMultiCoupon()); + + expect(result.current.shouldDisableCouponForm).toBe(true); + }); + + it('returns true when both submitting and pending', () => { + checkoutState.statuses.isSubmittingOrder.mockReturnValue(true); + checkoutState.statuses.isPending.mockReturnValue(true); + + const { result } = renderHook(() => useMultiCoupon()); + + expect(result.current.shouldDisableCouponForm).toBe(true); + }); + }); }); diff --git a/packages/core/src/app/coupon/useMultiCoupon.ts b/packages/core/src/app/coupon/useMultiCoupon.ts index cc185fcc50..8e5b2ec293 100644 --- a/packages/core/src/app/coupon/useMultiCoupon.ts +++ b/packages/core/src/app/coupon/useMultiCoupon.ts @@ -1,3 +1,5 @@ +import { useState } from 'react'; + import { useCheckout } from '@bigcommerce/checkout/contexts'; import { EMPTY_ARRAY } from '../common/utility'; @@ -6,14 +8,24 @@ interface UseMultiCouponValues { appliedCoupons: Array<{ code: string }>; appliedGiftCertificates: Array<{ code: string }>; applyCouponOrGiftCertificate: (code: string) => Promise; + couponError: string | null; + isCouponCodeCollapsed: boolean; removeCoupon: (code: string) => Promise; removeGiftCertificate: (giftCertificateCode: string) => Promise; - isCouponCodeCollapsed: boolean; + setCouponError: (error: string | null) => void; + shouldDisableCouponForm: boolean; } export const useMultiCoupon = (): UseMultiCouponValues => { + const [couponError, setCouponError] = useState(null); + const { checkoutState, checkoutService } = useCheckout(); - const config = checkoutState.data.getConfig(); + const { + data: { getConfig }, + statuses: { isSubmittingOrder, isPending } + } = checkoutState; + const config = getConfig(); + const shouldDisableCouponForm = isSubmittingOrder() || isPending(); if (!config) { throw new Error('Checkout configuration is not available'); @@ -57,8 +69,11 @@ export const useMultiCoupon = (): UseMultiCouponValues => { appliedCoupons, appliedGiftCertificates, applyCouponOrGiftCertificate, - removeGiftCertificate, - removeCoupon, + couponError, isCouponCodeCollapsed: config.checkoutSettings.isCouponCodeCollapsed, + removeCoupon, + removeGiftCertificate, + setCouponError, + shouldDisableCouponForm, }; }; diff --git a/packages/core/src/app/order/OrderSummary.tsx b/packages/core/src/app/order/OrderSummary.tsx index 476f024648..dc469bf1d2 100644 --- a/packages/core/src/app/order/OrderSummary.tsx +++ b/packages/core/src/app/order/OrderSummary.tsx @@ -46,7 +46,7 @@ const OrderSummary: FunctionComponent removeBundledItems(lineItems), [lineItems]); const displayInclusiveTax = isTaxIncluded && taxes && taxes.length > 0; diff --git a/packages/core/src/app/order/OrderSummaryModal.tsx b/packages/core/src/app/order/OrderSummaryModal.tsx index 51ab871217..05ec68b311 100644 --- a/packages/core/src/app/order/OrderSummaryModal.tsx +++ b/packages/core/src/app/order/OrderSummaryModal.tsx @@ -54,7 +54,7 @@ const OrderSummaryModal: FunctionComponent< }) => { const { checkoutState } = useCheckout(); const { checkoutSettings } = checkoutState.data.getConfig() ?? {}; - const isMultiCouponEnabled = isExperimentEnabled(checkoutSettings, 'PROJECT-7321-5991.multi-coupon-cart-checkout', false); + const isMultiCouponEnabled = isExperimentEnabled(checkoutSettings, 'CHECKOUT-9674.multi_coupon_cart_checkout', false); const displayInclusiveTax = isTaxIncluded && taxes && taxes.length > 0; diff --git a/packages/core/src/app/payment/PaymentForm.tsx b/packages/core/src/app/payment/PaymentForm.tsx index 21579ce769..bd93743469 100644 --- a/packages/core/src/app/payment/PaymentForm.tsx +++ b/packages/core/src/app/payment/PaymentForm.tsx @@ -110,7 +110,7 @@ const PaymentForm: FunctionComponent< const { checkoutState } = useCheckout(); const { checkoutSettings } = checkoutState.data.getConfig() ?? {}; - const isMultiCouponEnabled = isExperimentEnabled(checkoutSettings, 'PROJECT-7321-5991.multi-coupon-cart-checkout', false); + const isMultiCouponEnabled = isExperimentEnabled(checkoutSettings, 'CHECKOUT-9674.multi_coupon_cart_checkout', false); if (shouldExecuteSpamCheck) { return ( diff --git a/packages/core/src/scss/components/checkout/coupon/_coupon.scss b/packages/core/src/scss/components/checkout/coupon/_coupon.scss index d9eb9e4157..d844adb9ba 100644 --- a/packages/core/src/scss/components/checkout/coupon/_coupon.scss +++ b/packages/core/src/scss/components/checkout/coupon/_coupon.scss @@ -57,7 +57,6 @@ justify-content: space-between; padding: spacing('quarter'); width: 100%; - margin-bottom: spacing('half'); gap: spacing('half'); word-break: auto-phrase;