+
{ children }
);
diff --git a/client/components/payment-delete-illustration/styles.scss b/client/components/payment-delete-illustration/styles.scss
index e667cf0a373..524a44b8b1e 100644
--- a/client/components/payment-delete-illustration/styles.scss
+++ b/client/components/payment-delete-illustration/styles.scss
@@ -1,13 +1,13 @@
.payment-delete-illustration {
&__wrapper {
- margin: 80px auto 24px auto;
+ margin: 59px auto 24px auto;
width: 35%;
}
&__illustrations {
position: relative;
margin: 0 auto;
- width: 122px;
+ width: 100px;
&::before {
content: ' ';
@@ -18,8 +18,9 @@
right: 0;
background: no-repeat url( 'assets/images/cards/unknown.svg?asset' );
transform: translate( -22%, -50% );
- width: 135px;
- height: 105px;
+ height: 78px;
+ width: 110px;
+ background-size: 110px 78px;
}
}
@@ -38,11 +39,11 @@
&__payment-cross-icon {
position: absolute;
top: -24px;
- right: -24px;
- width: 45px;
+ right: -16px;
+ width: 35px;
height: auto;
fill: #cc1818;
- background: $white;
+ background: #fff;
border-radius: 999px;
}
}
diff --git a/client/components/payment-method-logos/index.tsx b/client/components/payment-method-logos/index.tsx
index 06c5b35d9f4..beb73857366 100644
--- a/client/components/payment-method-logos/index.tsx
+++ b/client/components/payment-method-logos/index.tsx
@@ -1,7 +1,8 @@
/**
* External dependencies
*/
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
+import { Popover } from '@wordpress/components';
/**
* Internal dependencies
@@ -17,7 +18,15 @@ import Affirm from 'assets/images/payment-method-icons/affirm.svg?asset';
import Klarna from 'assets/images/payment-method-icons/klarna.svg?asset';
import Jcb from 'assets/images/payment-method-icons/jcb.svg?asset';
import GooglePay from 'assets/images/payment-method-icons/gpay.svg?asset';
-
+import Cartebancaire from 'assets/images/cards/cartes_bancaires.svg?asset';
+import UnionPay from 'assets/images/cards/unionpay.svg?asset';
+import Diners from 'assets/images/cards/diners.svg?asset';
+import Eftpos from 'assets/images/cards/eftpos.svg?asset';
+import Ideal from 'assets/images/payment-methods/ideal.svg?asset';
+import Bancontact from 'assets/images/payment-methods/bancontact.svg?asset';
+import Eps from 'assets/images/payment-methods/eps.svg?asset';
+import Becs from 'assets/images/payment-methods/becs.svg?asset';
+import Przelewy24 from 'assets/images/payment-methods/przelewy24.svg?asset';
import './style.scss';
const PaymentMethods = [
@@ -61,17 +70,76 @@ const PaymentMethods = [
name: 'klarna',
component: Klarna,
},
+ {
+ name: 'cartebancaire',
+ component: Cartebancaire,
+ },
+ {
+ name: 'unionpay',
+ component: UnionPay,
+ },
+ {
+ name: 'diners',
+ component: Diners,
+ },
+ {
+ name: 'eftpos',
+ component: Eftpos,
+ },
{
name: 'jcb',
component: Jcb,
},
+ {
+ name: 'bancontact',
+ component: Bancontact,
+ },
+ {
+ name: 'becs',
+ component: Becs,
+ },
+ {
+ name: 'eps',
+ component: Eps,
+ },
+ {
+ name: 'ideal',
+ component: Ideal,
+ },
+ {
+ name: 'przelewy24',
+ component: Przelewy24,
+ },
];
-export const WooPaymentMethodsLogos: React.VFC< {
+export const WooPaymentsMethodsLogos: React.VFC< {
maxElements: number;
-} > = ( { maxElements = 10 } ) => {
+ isWooPayEligible: boolean;
+} > = ( {
+ maxElements = 10,
+ /**
+ * Whether the store (location) is eligible for WooPay.
+ * Based on this we will include or not the WooPay logo in the list.
+ */
+ isWooPayEligible = false,
+} ) => {
const totalPaymentMethods = 20;
const [ maxShownElements, setMaxShownElements ] = useState( maxElements );
+ const [ isPopoverVisible, setIsPopoverVisible ] = useState( false );
+ const popoverTimeoutRef = useRef< NodeJS.Timeout >();
+
+ // Reduce the total number of payment methods by one if the store is not eligible for WooPay.
+ const maxSupportedPaymentMethods = isWooPayEligible
+ ? totalPaymentMethods
+ : totalPaymentMethods - 1;
+
+ const getMaxShownElements = ( maxElementsNumber: number ) => {
+ if ( ! isWooPayEligible ) {
+ return maxElementsNumber + 1;
+ }
+
+ return maxElementsNumber;
+ };
useEffect( () => {
const updateMaxElements = () => {
@@ -88,10 +156,43 @@ export const WooPaymentMethodsLogos: React.VFC< {
window.addEventListener( 'resize', updateMaxElements );
}, [ maxElements ] );
+ const visiblePaymentMethods = PaymentMethods.slice(
+ 0,
+ getMaxShownElements( maxShownElements )
+ ).filter( ( pm ) => isWooPayEligible || pm.name !== 'woopay' );
+
+ const hiddenPaymentMethods = PaymentMethods.slice(
+ getMaxShownElements( maxShownElements )
+ ).filter( ( pm ) => isWooPayEligible || pm.name !== 'woopay' );
+
+ const showPopover = () => {
+ if ( popoverTimeoutRef.current ) {
+ clearTimeout( popoverTimeoutRef.current );
+ }
+ setIsPopoverVisible( true );
+ };
+
+ const hidePopover = () => {
+ // Add a delay before hiding the popover
+ popoverTimeoutRef.current = setTimeout( () => {
+ setIsPopoverVisible( false );
+ }, 300 ); // 300ms delay
+ };
+
+ // Cleanup timeout on unmount
+ useEffect( () => {
+ return () => {
+ if ( popoverTimeoutRef.current ) {
+ clearTimeout( popoverTimeoutRef.current );
+ }
+ };
+ }, [] );
+
return (
- <>
-
- { PaymentMethods.slice( 0, maxShownElements ).map( ( pm ) => {
+
+ { visiblePaymentMethods
+ .slice( 0, maxShownElements )
+ .map( ( pm ) => {
return (
);
} ) }
- { maxShownElements < totalPaymentMethods && (
-
- + { totalPaymentMethods - maxShownElements }
-
- ) }
-
- >
+ { maxShownElements < maxSupportedPaymentMethods && (
+
setIsPopoverVisible( ! isPopoverVisible ) }
+ onMouseEnter={ showPopover }
+ onMouseLeave={ hidePopover }
+ role="button"
+ tabIndex={ 0 }
+ onKeyDown={ ( event ) => {
+ if ( event.key === 'Enter' || event.key === ' ' ) {
+ setIsPopoverVisible( ! isPopoverVisible );
+ }
+ } }
+ >
+ + { maxSupportedPaymentMethods - maxShownElements }
+ { isPopoverVisible && (
+
setIsPopoverVisible( false ) }
+ onMouseEnter={ showPopover }
+ onMouseLeave={ hidePopover }
+ >
+
+ { hiddenPaymentMethods.map( ( pm ) => {
+ return (
+
+ );
+ } ) }
+
+
+ ) }
+
+ ) }
+
);
};
diff --git a/client/components/payment-method-logos/style.scss b/client/components/payment-method-logos/style.scss
index ea36db24ecc..70081fb6a08 100644
--- a/client/components/payment-method-logos/style.scss
+++ b/client/components/payment-method-logos/style.scss
@@ -1,14 +1,13 @@
.connect-account-page__payment-methods--logos {
display: flex;
align-items: center;
- border: 1px solid $gray-300;
- border-bottom: none;
- padding: 12px;
+ padding: $gap-small 0;
+ gap: $gap-smaller;
+
img {
- width: 37px;
+ width: 38px;
height: 24px;
- margin-right: 8px;
- border: 1px solid $gray-300;
+ border: 1px solid $gray-100;
border-radius: 3px;
}
@@ -23,4 +22,31 @@
font-size: 11px;
font-weight: 600;
}
+
+ &-count:hover {
+ cursor: pointer;
+ color: rgba( 56, 88, 233, 1 );
+ background-color: rgba( 56, 88, 233, 0.15 );
+ border-color: rgba( 56, 88, 233, 1 );
+ }
+
+ &-inside-popover {
+ display: grid;
+ grid-template-columns: repeat( 4, 1fr );
+ grid-template-rows: repeat( 3, 1fr );
+ gap: $gap-smaller;
+ padding: 0;
+ }
+
+ &-popover {
+ .components-popover__content {
+ background: $white;
+ border: 1px solid $gray-100;
+ border-radius: 3px;
+ max-width: 246px;
+ padding: $gap-smaller;
+ box-shadow: none;
+ margin-top: $gap-smaller;
+ }
+ }
}
diff --git a/client/components/payment-methods-checkboxes/test/index.test.tsx b/client/components/payment-methods-checkboxes/test/index.test.tsx
index 59b231190e0..afbf32084b5 100644
--- a/client/components/payment-methods-checkboxes/test/index.test.tsx
+++ b/client/components/payment-methods-checkboxes/test/index.test.tsx
@@ -35,11 +35,9 @@ describe( 'PaymentMethodsCheckboxes', () => {
const upeMethods = [
[ 'bancontact', true ],
[ 'eps', false ],
- [ 'giropay', false ],
[ 'ideal', false ],
[ 'p24', false ],
[ 'sepa_debit', false ],
- [ 'sofort', false ],
];
render(
@@ -62,29 +60,23 @@ describe( 'PaymentMethodsCheckboxes', () => {
const paymentMethods = screen.getAllByRole( 'listitem' );
const bancontact = within( paymentMethods[ 0 ] );
const eps = within( paymentMethods[ 1 ] );
- const giropay = within( paymentMethods[ 2 ] );
- const ideal = within( paymentMethods[ 3 ] );
- const p24 = within( paymentMethods[ 4 ] );
- const sepa = within( paymentMethods[ 5 ] );
- const sofort = within( paymentMethods[ 6 ] );
+ const ideal = within( paymentMethods[ 2 ] );
+ const p24 = within( paymentMethods[ 3 ] );
+ const sepa = within( paymentMethods[ 4 ] );
expect( bancontact.getByRole( 'checkbox' ) ).toBeChecked();
expect( eps.getByRole( 'checkbox' ) ).not.toBeChecked();
- expect( giropay.getByRole( 'checkbox' ) ).not.toBeChecked();
expect( ideal.getByRole( 'checkbox' ) ).not.toBeChecked();
expect( p24.getByRole( 'checkbox' ) ).not.toBeChecked();
expect( sepa.getByRole( 'checkbox' ) ).not.toBeChecked();
- expect( sofort.getByRole( 'checkbox' ) ).not.toBeChecked();
jest.useFakeTimers();
act( () => {
userEvent.click( bancontact.getByRole( 'checkbox' ) );
userEvent.click( eps.getByRole( 'checkbox' ) );
- userEvent.click( giropay.getByRole( 'checkbox' ) );
userEvent.click( ideal.getByRole( 'checkbox' ) );
userEvent.click( p24.getByRole( 'checkbox' ) );
userEvent.click( sepa.getByRole( 'checkbox' ) );
- userEvent.click( sofort.getByRole( 'checkbox' ) );
jest.runOnlyPendingTimers();
} );
@@ -95,11 +87,9 @@ describe( 'PaymentMethodsCheckboxes', () => {
'bancontact',
false
);
- expect( handleChange ).toHaveBeenNthCalledWith( 3, 'giropay', true );
- expect( handleChange ).toHaveBeenNthCalledWith( 4, 'ideal', true );
- expect( handleChange ).toHaveBeenNthCalledWith( 5, 'p24', true );
- expect( handleChange ).toHaveBeenNthCalledWith( 6, 'sepa_debit', true );
- expect( handleChange ).toHaveBeenNthCalledWith( 7, 'sofort', true );
+ expect( handleChange ).toHaveBeenNthCalledWith( 3, 'ideal', true );
+ expect( handleChange ).toHaveBeenNthCalledWith( 4, 'p24', true );
+ expect( handleChange ).toHaveBeenNthCalledWith( 5, 'sepa_debit', true );
jest.useRealTimers();
} );
@@ -108,10 +98,10 @@ describe( 'PaymentMethodsCheckboxes', () => {
render(
{
);
- const sofortCheckbox = screen.getByRole( 'checkbox', {
- name: 'Sofort',
+ const idealCheckbox = screen.getByRole( 'checkbox', {
+ name: 'iDEAL',
} );
- expect( sofortCheckbox ).not.toBeChecked();
+ expect( idealCheckbox ).not.toBeChecked();
jest.useFakeTimers();
act( () => {
- userEvent.click( sofortCheckbox );
+ userEvent.click( idealCheckbox );
jest.runOnlyPendingTimers();
} );
expect( handleChange ).toHaveBeenCalledTimes( 1 );
- expect( handleChange ).toHaveBeenNthCalledWith( 1, 'sofort', true );
+ expect( handleChange ).toHaveBeenNthCalledWith( 1, 'ideal', true );
jest.useRealTimers();
} );
@@ -161,10 +151,10 @@ describe( 'PaymentMethodsCheckboxes', () => {
const page = render(
{
render(
{
/>
);
- const sofortCheckbox = screen.getByRole( 'checkbox', {
- name: 'Sofort',
+ const idealCheckbox = screen.getByRole( 'checkbox', {
+ name: 'iDEAL',
} );
- expect( sofortCheckbox ).not.toBeChecked();
- userEvent.click( sofortCheckbox );
+ expect( idealCheckbox ).not.toBeChecked();
+ userEvent.click( idealCheckbox );
expect( handleChange ).toHaveBeenCalledTimes( 0 ); // Because the input is disabled.
- expect( sofortCheckbox ).not.toBeChecked();
+ expect( idealCheckbox ).not.toBeChecked();
} );
it( "doesn't show the disabled notice pill on payment methods with active and unrequested statuses", () => {
@@ -233,20 +223,20 @@ describe( 'PaymentMethodsCheckboxes', () => {
render(
= ( {
<>
{ interpolateComponents( {
mixedString: sprintf(
- /* translators: %1$s: WooPayments */
+ /* translators: 1: WooPayments */
__(
// eslint-disable-next-line max-len
- 'Sandbox mode gives you access to all %1$s features while checkout transactions are simulated. {{learnMoreLink}}Learn more{{/learnMoreLink}}',
+ 'A test account gives you access to all %1$s features while checkout transactions are simulated. {{learnMoreLink}}Learn more{{/learnMoreLink}}',
'woocommerce-payments'
),
'WooPayments'
diff --git a/client/components/stripe-spinner/index.tsx b/client/components/stripe-spinner/index.tsx
new file mode 100644
index 00000000000..eec0c9cae92
--- /dev/null
+++ b/client/components/stripe-spinner/index.tsx
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+import React from 'react';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+/**
+ * Copy of the Stripe spinner in order to avoid pulling another Stripe dependency.
+ */
+const StripeSpinner = () => {
+ return (
+
+ );
+};
+
+export default StripeSpinner;
diff --git a/client/components/stripe-spinner/style.scss b/client/components/stripe-spinner/style.scss
new file mode 100644
index 00000000000..f8e9fc68816
--- /dev/null
+++ b/client/components/stripe-spinner/style.scss
@@ -0,0 +1,32 @@
+/* These keyframes and spinner style are copied from the Stripe Spinner component https://docs.stripe.com/stripe-apps/components/spinner?app-sdk-version=8 */
+
+@keyframes SpinnerAnimationShow {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes SpinnerAnimationRotation {
+ 0% {
+ transform: scaleX( -1 ) rotateZ( 0deg );
+ }
+ 50% {
+ transform: scaleX( -1 ) rotateZ( -180deg );
+ }
+ 100% {
+ transform: scaleX( -1 ) rotateZ( -360deg );
+ }
+}
+
+.stripe-spinner {
+ animation: SpinnerAnimationShow 250ms ease,
+ SpinnerAnimationRotation 0.7s linear infinite; // The timings are exactly like Stripe Spinner component.
+ transition-timing-function: ease;
+ transition-property: opacity, transform;
+ transform-origin: 50% 50%;
+ display: inline-block;
+ vertical-align: middle; // Support stripe loader inside the buttons.
+}
diff --git a/client/connect-account-page/index.tsx b/client/connect-account-page/index.tsx
index 42cb5f25f88..bc49d16cdfd 100644
--- a/client/connect-account-page/index.tsx
+++ b/client/connect-account-page/index.tsx
@@ -31,7 +31,7 @@ import SetupImg from 'assets/images/illustrations/setup.svg?asset';
import strings from './strings';
import './style.scss';
import InlineNotice from 'components/inline-notice';
-import { WooPaymentMethodsLogos } from 'components/payment-method-logos';
+import { WooPaymentsMethodsLogos } from 'components/payment-method-logos';
import WooLogo from 'assets/images/woo-logo.svg?asset';
import { sanitizeHTML } from 'wcpay/utils/sanitize';
import { isInTestModeOnboarding } from 'wcpay/utils';
@@ -56,7 +56,11 @@ const TestDriveLoader: React.FunctionComponent< {
-
+
@@ -464,7 +468,11 @@ const ConnectAccountPage: React.FC = () => {
}
return (
-
+
{ errorMessage && (
{