Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add payment method logos to card label #10005

Merged
merged 26 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cbeab48
Add payment method logos to blocks card label
mdmoore Dec 19, 2024
832078e
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Dec 20, 2024
cf07201
Changelog
mdmoore Dec 20, 2024
cd49ec3
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 6, 2025
13c615e
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 8, 2025
a71bb0b
Re-add non-card logos
mdmoore Jan 8, 2025
5935f3a
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 13, 2025
884925f
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 16, 2025
9cd0f11
Add card brand logos to classic checkout
mdmoore Jan 21, 2025
117d653
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 21, 2025
5d980b7
Update Credit Card / Debit Card label to Cards (#9769)
gpressutto5 Jan 22, 2025
83dc299
Update settings UI text for credit / debit cards
mdmoore Jan 22, 2025
3212b67
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 22, 2025
858b9a2
Update tests
mdmoore Jan 22, 2025
19a7a12
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 22, 2025
ffd2f87
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 23, 2025
2d30b89
Better responsive handling
mdmoore Jan 27, 2025
64bb4e2
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 27, 2025
51d62a3
Merge branch 'develop' into add/9826-payment-methods-logos-component
gpressutto5 Jan 28, 2025
5c29d6d
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 31, 2025
ca885bb
Update e2e tests
mdmoore Jan 31, 2025
a50b419
Merge branch 'develop' into add/9826-payment-methods-logos-component
mdmoore Jan 31, 2025
1b83a56
Update e2e tests
mdmoore Feb 1, 2025
5f6fdc0
Update subscriptions e2e test
mdmoore Feb 3, 2025
a691f2a
Only inject payment logos once
mdmoore Feb 4, 2025
f78457c
Remove currency from E2E test
mdmoore Feb 4, 2025
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
4 changes: 4 additions & 0 deletions changelog/add-9826-payment-methods-logos-component
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add payment method logos to checkout block card label.
4 changes: 4 additions & 0 deletions changelog/update-cards-label
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: update

Update Credit Card / Debit Card label to Cards
56 changes: 49 additions & 7 deletions client/checkout/blocks/payment-method-label.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import {
Elements,
PaymentMethodMessagingElement,
} from '@stripe/react-stripe-js';
import { PaymentMethodsLogos } from './payment-methods-logos';
import Visa from 'assets/images/payment-method-icons/visa.svg?asset';
import Mastercard from 'assets/images/payment-method-icons/mastercard.svg?asset';
import Amex from 'assets/images/payment-method-icons/amex.svg?asset';
import Discover from 'assets/images/payment-method-icons/discover.svg?asset';
import { normalizeCurrencyToMinorUnit } from '../utils';
import { useStripeForUPE } from 'wcpay/hooks/use-stripe-async';
import { getUPEConfig } from 'wcpay/utils/checkout';
Expand All @@ -13,6 +18,32 @@ import './style.scss';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { getAppearance, getFontRulesFromPage } from 'wcpay/checkout/upe-styles';

const paymentMethods = [
{
name: 'visa',
component: Visa,
},
{
name: 'mastercard',
component: Mastercard,
},
{
name: 'amex',
component: Amex,
},
{
name: 'discover',
component: Discover,
},
// TODO: Missing Diners Club
// TODO: What other card payment methods should be here?
];
const breakpointConfigs = [
{ breakpoint: 550, maxElements: 2 },
{ breakpoint: 833, maxElements: 4 },
{ breakpoint: 960, maxElements: 2 },
];

const bnplMethods = [ 'affirm', 'afterpay_clearpay', 'klarna' ];
const PaymentMethodMessageWrapper = ( {
upeName,
Expand Down Expand Up @@ -53,6 +84,7 @@ export default ( { api, title, countries, iconLight, iconDark, upeName } ) => {
const [ appearance, setAppearance ] = useState(
getUPEConfig( 'wcBlocksUPEAppearance' )
);

const [ upeAppearanceTheme, setUpeAppearanceTheme ] = useState(
getUPEConfig( 'wcBlocksUPEAppearanceTheme' )
);
Expand Down Expand Up @@ -106,13 +138,23 @@ export default ( { api, title, countries, iconLight, iconDark, upeName } ) => {
{ __( 'Test Mode', 'woocommerce-payments' ) }
</span>
) }
<img
className="payment-methods--logos"
src={
upeAppearanceTheme === 'night' ? iconDark : iconLight
}
alt={ title }
/>
{ upeName === 'card' ? (
<PaymentMethodsLogos
maxElements={ 4 }
paymentMethods={ paymentMethods }
breakpointConfigs={ breakpointConfigs }
/>
) : (
<img
className="payment-methods--logos"
src={
upeAppearanceTheme === 'night'
? iconDark
: iconLight
}
alt={ title }
/>
) }
</div>
<PaymentMethodMessageWrapper
upeName={ upeName }
Expand Down
1 change: 1 addition & 0 deletions client/checkout/blocks/payment-methods-logos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PaymentMethodsLogos } from './payment-methods-logos';
139 changes: 139 additions & 0 deletions client/checkout/blocks/payment-methods-logos/logo-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* External dependencies
*/
import React, {
useEffect,
useLayoutEffect,
useRef,
useState,
useCallback,
} from 'react';

interface LogoPopoverProps {
id: string;
className?: string;
children: React.ReactNode;
anchor: HTMLElement | null;
open: boolean;
onClose?: () => void;
dataTestId?: string;
}

export const LogoPopover: React.FC< LogoPopoverProps > = ( {
id,
className,
children,
anchor,
open,
onClose,
dataTestId,
} ) => {
const popoverRef = useRef< HTMLDivElement >( null );
const [ isPositioned, setIsPositioned ] = useState( false );

const updatePosition = useCallback( () => {
const popover = popoverRef.current;
if ( ! popover || ! anchor ) {
return;
}

// Get the most up-to-date anchor rect
const anchorRect = anchor.getBoundingClientRect();

// Temporarily make the popover visible to get correct dimensions
popover.style.visibility = 'hidden';
popover.style.display = 'block';
const popoverRect = popover.getBoundingClientRect();
popover.style.display = '';
popover.style.visibility = '';

const offset = 7;
const left = anchorRect.left;
// Position the popover above the anchor
const top = anchorRect.top - popoverRect.height - offset;

popover.style.position = 'fixed';
popover.style.width = `${ anchorRect.width }px`;
popover.style.left = `${ left }px`;
popover.style.top = `${ top }px`;

// Adjust position if popover goes off-screen
if ( top < 0 ) {
// If there's not enough space above, position it below the anchor
popover.style.top = `${ anchorRect.bottom + offset }px`;
}

setIsPositioned( true );
}, [ anchor ] );

useLayoutEffect( () => {
if ( open && anchor ) {
// Use requestAnimationFrame to ensure the DOM has updated before positioning
requestAnimationFrame( updatePosition );
}
}, [ open, anchor, updatePosition ] );

useEffect( () => {
if ( open && anchor ) {
const observer = new MutationObserver( updatePosition );
observer.observe( anchor, {
attributes: true,
childList: true,
subtree: true,
} );

window.addEventListener( 'resize', updatePosition );
window.addEventListener( 'scroll', updatePosition );

const handleOutsideClick = ( event: MouseEvent ) => {
if (
popoverRef.current &&
! popoverRef.current.contains( event.target as Node ) &&
! anchor.contains( event.target as Node )
) {
onClose?.();
}
};

const handleEscapeKey = ( event: KeyboardEvent ) => {
if ( event.key === 'Escape' ) {
onClose?.();
}
};

document.addEventListener( 'mousedown', handleOutsideClick );
document.addEventListener( 'keydown', handleEscapeKey );

return () => {
observer.disconnect();
window.removeEventListener( 'resize', updatePosition );
window.removeEventListener( 'scroll', updatePosition );
document.removeEventListener( 'mousedown', handleOutsideClick );
document.removeEventListener( 'keydown', handleEscapeKey );
};
}
}, [ open, anchor, updatePosition, onClose ] );

if ( ! open ) {
return null;
}

return (
<div
id={ id }
ref={ popoverRef }
className={ `logo-popover ${ className || '' }` }
style={ {
position: 'fixed',
zIndex: 1000,
opacity: isPositioned ? 1 : 0,
transition: 'opacity 0.2s',
} }
role="dialog"
aria-label="Supported Credit Card Brands"
data-testid={ dataTestId }
>
{ children }
</div>
);
};
143 changes: 143 additions & 0 deletions client/checkout/blocks/payment-methods-logos/payment-methods-logos.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* External dependencies
*/
import React, { useState, useEffect, useCallback, useRef } from 'react';
/**
* Internal dependencies
*/
import { LogoPopover } from './logo-popover';
import './style.scss';

interface BreakpointConfig {
breakpoint: number;
maxElements: number;
}

interface PaymentMethodsLogosProps {
maxElements: number;
paymentMethods: { name: string; component: string }[];
breakpointConfigs?: BreakpointConfig[];
}

const breakpointConfigsDefault = [
{ breakpoint: 480, maxElements: 5 },
{ breakpoint: 768, maxElements: 7 },
];
const paymentMethodsDefault: never[] = [];
export const PaymentMethodsLogos: React.FC< PaymentMethodsLogosProps > = ( {
maxElements = 10,
paymentMethods = paymentMethodsDefault,
breakpointConfigs = breakpointConfigsDefault,
} ) => {
const [ maxShownElements, setMaxShownElements ] = useState( maxElements );
const [
popoverAnchor,
setPopoverAnchor,
] = useState< HTMLDivElement | null >( null );
const [ popoverOpen, setPopoverOpen ] = useState( false );
const [ shouldHavePopover, setShouldHavePopover ] = useState( false );

const togglePopover = () => setPopoverOpen( ! popoverOpen );

const anchorRef = useCallback( ( node: HTMLDivElement | null ) => {
if ( node !== null ) {
setPopoverAnchor( node );
}
}, [] );

const buttonRef = useRef< HTMLDivElement | null >( null );

const handlePopoverClose = useCallback( () => {
setPopoverOpen( false );
buttonRef.current?.focus();
}, [] );

useEffect( () => {
const updateMaxElements = () => {
const sortedConfigs = [ ...breakpointConfigs ].sort(
( a, b ) => a.breakpoint - b.breakpoint
);
const config = sortedConfigs.find(
( cfg ) => window.innerWidth <= cfg.breakpoint
);

setMaxShownElements( config ? config.maxElements : maxElements );
};

updateMaxElements();
window.addEventListener( 'resize', updateMaxElements );

return () => window.removeEventListener( 'resize', updateMaxElements );
}, [ breakpointConfigs, maxElements ] );

useEffect( () => {
if ( popoverAnchor ) {
buttonRef.current = popoverAnchor;
}
}, [ popoverAnchor ] );

useEffect( () => {
setShouldHavePopover( paymentMethods.length > maxShownElements );
}, [ maxShownElements, paymentMethods.length ] );

return (
<>
<div className="payment-methods--logos">
<div
ref={ anchorRef }
{ ...( shouldHavePopover && {
onClick: togglePopover,
onKeyDown: ( e ) => {
if ( e.key === 'Enter' || e.key === ' ' ) {
e.preventDefault();
togglePopover();
}
},
role: 'button',
tabIndex: 0,
'aria-expanded': popoverOpen,
'aria-controls': 'payment-methods-popover',
} ) }
data-testid="payment-methods-logos"
>
{ paymentMethods
.slice( 0, maxShownElements )
.map( ( pm ) => (
<img
key={ pm.name }
alt={ pm.name }
src={ pm.component }
width={ 38 }
height={ 24 }
/>
) ) }
{ shouldHavePopover && (
<div className="payment-methods--logos-count">
+ { paymentMethods.length - maxShownElements }
</div>
) }
</div>
</div>
{ shouldHavePopover && popoverOpen && (
<LogoPopover
id="payment-methods-popover"
className="payment-methods--logos-popover"
anchor={ popoverAnchor }
open={ popoverOpen }
onClose={ handlePopoverClose }
dataTestId="payment-methods-popover"
>
{ paymentMethods.slice( maxShownElements ).map( ( pm ) => (
<img
key={ pm.name }
alt={ pm.name }
src={ pm.component }
width={ 38 }
height={ 24 }
/>
) ) }
</LogoPopover>
) }
</>
);
};
Loading
Loading