Skip to content
Open
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
38 changes: 38 additions & 0 deletions packages/core/src/app/payment/PaymentForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,44 @@ describe('PaymentForm', () => {
expect(radios[1]).not.toBeChecked();
});

it('does not render the loading overlay while a payment method is initializing in enhancedThemeV1', () => {
enhancedThemeV1 = true;

render(<PaymentFormTest {...defaultProps} isInitializingPayment={true} />);

expect(screen.queryByTestId('loading-overlay')).not.toBeInTheDocument();
expect(screen.getAllByRole('radio')).toHaveLength(2);
});

it('keeps payment methods unselectable while a payment method is initializing in enhancedThemeV1', () => {
enhancedThemeV1 = true;

const onMethodSelect = jest.fn();

render(
<PaymentFormTest
{...defaultProps}
isInitializingPayment={true}
onMethodSelect={onMethodSelect}
/>,
);

const radios = screen.getAllByRole('radio');

fireEvent.click(radios[1]);

expect(onMethodSelect).not.toHaveBeenCalled();
expect(radios[1]).not.toBeChecked();
});

it('renders the payment method skeleton for the initializing method in enhancedThemeV1', () => {
enhancedThemeV1 = true;

render(<PaymentFormTest {...defaultProps} isInitializingPayment={true} />);

expect(screen.getByTestId('payment-method-skeleton')).toBeInTheDocument();
});

it('renders terms and conditions field if copy is provided', () => {
const textAcceptTerms = 'Accept terms';

Expand Down
102 changes: 71 additions & 31 deletions packages/core/src/app/payment/paymentMethod/PaymentMethodList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import { type PaymentMethod } from '@bigcommerce/checkout-sdk';
import { find, noop } from 'lodash';
import React, { type FunctionComponent, memo, useCallback, useMemo } from 'react';

import { useCheckout, useLocale } from '@bigcommerce/checkout/contexts';
import { Checklist, ChecklistItem, LoadingOverlay } from '@bigcommerce/checkout/ui';
import { useCheckout, useLocale, useThemeContext } from '@bigcommerce/checkout/contexts';
import {
Checklist,
ChecklistItem,
LoadingOverlay,
PaymentMethodSkeleton,
} from '@bigcommerce/checkout/ui';

import { connectFormik, type ConnectFormikProps } from '../../common/form';

Expand Down Expand Up @@ -47,6 +52,7 @@ const PaymentMethodList: FunctionComponent<
}) => {
const { language } = useLocale();
const { selectedState: config } = useCheckout(({ data }) => data.getConfig());
const { enhancedThemeV1 } = useThemeContext();

const chequeMethod = find(methods, { id: 'cheque' });
const chequeDisabledReason = usePoMethodDisabledReason(chequeMethod);
Expand Down Expand Up @@ -81,37 +87,44 @@ const PaymentMethodList: FunctionComponent<
[methods, onSelect],
);

const checklist = (
<Checklist
defaultSelectedItemId={values.paymentProviderRadio}
name="paymentProviderRadio"
onSelect={handleSelect}
>
{methods.map((method) => {
const value = getUniquePaymentMethodId(method.id, method.gateway);

return (
<PaymentMethodListItem
disabledReason={method === chequeMethod ? chequeDisabledReason : undefined}
isEmbedded={isEmbedded}
isInitializingPayment={isInitializingPayment}
isSelected={value === values.paymentProviderRadio}
isUsingMultiShipping={isUsingMultiShipping}
key={value}
method={method}
onUnhandledError={onUnhandledError}
value={value}
/>
);
})}
</Checklist>
);

return (
<>
<div aria-live="assertive" className="is-srOnly" role="status">
{titleText}
</div>
<LoadingOverlay isLoading={Boolean(isInitializingPayment)}>
<Checklist
defaultSelectedItemId={values.paymentProviderRadio}
name="paymentProviderRadio"
onSelect={handleSelect}
>
{methods.map((method) => {
const value = getUniquePaymentMethodId(method.id, method.gateway);

return (
<PaymentMethodListItem
disabledReason={
method === chequeMethod ? chequeDisabledReason : undefined
}
isEmbedded={isEmbedded}
isInitializingPayment={isInitializingPayment}
isUsingMultiShipping={isUsingMultiShipping}
key={value}
method={method}
onUnhandledError={onUnhandledError}
value={value}
/>
);
})}
</Checklist>
</LoadingOverlay>
{enhancedThemeV1 ? (
checklist
) : (
<LoadingOverlay isLoading={Boolean(isInitializingPayment)}>
{checklist}
</LoadingOverlay>
)}
</>
);
};
Expand All @@ -120,6 +133,7 @@ interface PaymentMethodListItemProps {
disabledReason?: PoDisabledReason;
isEmbedded?: boolean;
isInitializingPayment?: boolean;
isSelected: boolean;
isUsingMultiShipping?: boolean;
method: PaymentMethod;
value: string;
Expand All @@ -130,21 +144,47 @@ const PaymentMethodListItem: FunctionComponent<PaymentMethodListItemProps> = ({
disabledReason,
isEmbedded,
isInitializingPayment,
isSelected,
isUsingMultiShipping,
method,
onUnhandledError,
value,
}) => {
const { enhancedThemeV1 } = useThemeContext();
const isCustomChecklistItem = Boolean(method.initializationData?.isCustomChecklistItem);

const renderPaymentMethod = useMemo(() => {
return (
const paymentMethod = (
<PaymentMethodV2
isEmbedded={isEmbedded}
isUsingMultiShipping={isUsingMultiShipping}
method={method}
onUnhandledError={onUnhandledError || noop}
/>
);
}, [isEmbedded, isUsingMultiShipping, method, onUnhandledError]);

// Custom checklist items manage their own loading UI
return enhancedThemeV1 && !isCustomChecklistItem ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Custom checklist items were previously covered by the list-wide overlay and now get nothing. The comment says they manage their own loading UI — is that true for all of them, or just the wallet buttons you checked? If any of them don't, they'll just sit there looking like a dead radio during init.

<LoadingOverlay
hideContentWhenLoading

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Heads up that hideContentWhenLoading sets display: none on the content rather than overlaying it, so the method initializes with no layout box.

No change for credit-card / hosted-payment / Adyen / Braintree / PayPal Commerce — they already self-wrap in this with the same flag. But the integrations that don't (Stripe, CheckoutCom, Mollie, Moneris, Worldpay) used to render visible under the overlay and now go fully hidden during init. Stripe UPE mounts Elements into #stripe-${method.id}-component-field right in that window.

Can you try to test Stripe manally? That one plus another hosted-field provider would be good to check.

isLoading={isSelected && Boolean(isInitializingPayment)}
loadingSkeleton={<PaymentMethodSkeleton />}
>
{paymentMethod}
</LoadingOverlay>
Comment thread
cursor[bot] marked this conversation as resolved.
) : (
paymentMethod
);
}, [
enhancedThemeV1,
isCustomChecklistItem,
isEmbedded,
isInitializingPayment,
isSelected,
isUsingMultiShipping,
method,
onUnhandledError,
]);

const renderPaymentMethodTitle = useCallback(
(isSelected: boolean) => (
Expand All @@ -158,7 +198,7 @@ const PaymentMethodListItem: FunctionComponent<PaymentMethodListItemProps> = ({
[disabledReason, method],
);

if (method.initializationData?.isCustomChecklistItem) {
if (isCustomChecklistItem) {
return <CustomChecklistItem content={renderPaymentMethod} htmlId={`radio-${value}`} />;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
@import "checkout/payment-billing";
@import "checkout/paymentProvider";
@import "checkout/loading-dots";
@import "checkout/payment-loading-skeleton";
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,16 @@
& ~ .form-label::before {
border: 2px $input-border-style $input-border-color;
}

// payments radio buttons should look non-interactive while initializing
&:disabled ~ .form-label,
&[aria-disabled="true"] ~ .form-label {
cursor: not-allowed;

&::before,
&::after {
opacity: $checkRadio-disabled-opacity;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@keyframes payment-method-skeleton-pulse {
0%, 100% {
background-color: color("greys", "lighter");
}

50% {
background-color: mix(color("greys", "light"), color("greys", "medium"), 50%);
}
}

.payment-method-skeleton {
@include enhancedThemeV1 {
padding: spacing("half") 0;

div {
animation: payment-method-skeleton-pulse 4000ms infinite cubic-bezier(0.13, 0.615, 0.315, 0.915);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💅
4000ms infinite cubic-bezier(0.13, 0.615, 0.315, 0.915) is character-for-character $loading-skeleton-grey-box-animation from settings/foundation/forms/_settings.scss. Worth pulling the timing from the variable so the two don't drift apart later — only the colour stops actually differ here.

background: color("greys", "lighter");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { type FunctionComponent } from 'react';

import ChecklistSkeleton from './ChecklistSkeleton';

export const PaymentMethodSkeleton: FunctionComponent = () => (
<div aria-busy="true" data-test="payment-method-skeleton" role="status">
<ChecklistSkeleton additionalClassName="payment-method-skeleton" rows={2} />
</div>
);
1 change: 1 addition & 0 deletions packages/ui/src/form/LoadingSkeleton/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as CheckoutPageSkeleton } from './CheckoutPageSkeleton';
export { default as OrderConfirmationPageSkeleton } from './OrderConfirmationPageSkeleton';
export { default as WalletButtonsContainerSkeleton } from './WalletButtonContainerSkeleton';
export { CartSummarySkeleton } from './CartSummarySkeleton';
export { PaymentMethodSkeleton } from './PaymentMethodSkeleton';
1 change: 1 addition & 0 deletions packages/ui/src/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
CheckoutPageSkeleton,
ChecklistSkeleton,
OrderConfirmationPageSkeleton,
PaymentMethodSkeleton,
WalletButtonsContainerSkeleton,
} from './LoadingSkeleton';
export { Label } from './Label';
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
CheckboxInput,
ChecklistSkeleton,
OrderConfirmationPageSkeleton,
PaymentMethodSkeleton,
Fieldset,
Form,
FormField,
Expand Down