Skip to content

Commit

Permalink
V5: Whitelabel Pay by Bank in the US (#2879)
Browse files Browse the repository at this point in the history
* initial pay by bank us work

* initial pay by bank us work

make config to always show logos

add changeset

fix tests

stored pay by bank and code review changes

some more tests and code rivew fixes

* fixes for v5

* fixes css on small screens

* translations

* fix css brands for v5

* remove console.log
  • Loading branch information
m1aw authored Oct 1, 2024
1 parent 098f006 commit dec2404
Show file tree
Hide file tree
Showing 37 changed files with 406 additions and 40 deletions.
5 changes: 5 additions & 0 deletions .changeset/afraid-phones-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': minor
---

Pay by Bank US now shows whitelabel branding
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { h } from 'preact';
import PaymentMethodIcon from '../PaymentMethodIcon';
import { BrandConfiguration } from '../../../../Card/types';
import { getFullBrandName } from '../../../../Card/components/CardInput/utils';
import useCoreContext from '../../../../../core/Context/useCoreContext';

interface CompactViewProps {
allowedBrands: Array<BrandConfiguration>; // A set of brands filtered to exclude those that can never appear in the UI
isPaymentMethodSelected: boolean;
keepBrandsVisible?: boolean;
showOtherInsteafOfNumber?: boolean;
}

const prepareVisibleBrands = (allowedBrands: Array<BrandConfiguration>) => {
Expand All @@ -16,8 +19,10 @@ const prepareVisibleBrands = (allowedBrands: Array<BrandConfiguration>) => {
};
};

const CompactView = ({ allowedBrands, isPaymentMethodSelected }: CompactViewProps) => {
if (isPaymentMethodSelected) {
const CompactView = ({ allowedBrands, isPaymentMethodSelected, showOtherInsteafOfNumber = false, keepBrandsVisible = false }: CompactViewProps) => {
const { i18n } = useCoreContext();

if (isPaymentMethodSelected && !keepBrandsVisible) {
return null;
}

Expand All @@ -27,7 +32,11 @@ const CompactView = ({ allowedBrands, isPaymentMethodSelected }: CompactViewProp
{visibleBrands.map(brand => (
<PaymentMethodIcon key={brand.name} altDescription={getFullBrandName(brand.name)} type={brand.name} src={brand.icon} />
))}
{leftBrandsAmount !== 0 && <span className="adyen-checkout__payment-method__brand-number">+{leftBrandsAmount}</span>}
{showOtherInsteafOfNumber ? (
<span className="adyen-checkout__payment-method__brand-number">+ {i18n.get('paymentMethodBrand.other')}</span>
) : (
leftBrandsAmount !== 0 && <span className="adyen-checkout__payment-method__brand-number">+{leftBrandsAmount}</span>
)}
</span>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,30 @@ interface PaymentMethodBrandsProps {
isPaymentMethodSelected: boolean;
activeBrand?: string;
isCompactView?: boolean;
keepBrandsVisible?: boolean;
showOtherInsteafOfNumber?: boolean;
}

const PaymentMethodBrands = ({ activeBrand, brands, excludedUIBrands, isPaymentMethodSelected, isCompactView = true }: PaymentMethodBrandsProps) => {
// A set of brands filtered to exclude those that can never appear in the UI
const PaymentMethodBrands = ({
brands,
excludedUIBrands,
isPaymentMethodSelected,
activeBrand,
isCompactView = true,
keepBrandsVisible = false,
showOtherInsteafOfNumber = false
}: PaymentMethodBrandsProps) => {
const allowedBrands = brands.filter(brand => !excludedUIBrands?.includes(brand.name));

if (isCompactView) {
return <CompactView allowedBrands={allowedBrands} isPaymentMethodSelected={isPaymentMethodSelected} />;
return (
<CompactView
allowedBrands={allowedBrands}
isPaymentMethodSelected={isPaymentMethodSelected}
showOtherInsteafOfNumber={showOtherInsteafOfNumber}
keepBrandsVisible={keepBrandsVisible}
/>
);
}
return (
<span className="adyen-checkout__payment-method__brands">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ class PaymentMethodItem extends Component<PaymentMethodItemProps> {
excludedUIBrands={BRAND_ICON_UI_EXCLUSION_LIST}
isPaymentMethodSelected={isSelected}
isCompactView={paymentMethod.props.showBrandsUnderCardNumber}
keepBrandsVisible={paymentMethod.props.keepBrandsVisible}
showOtherInsteafOfNumber={paymentMethod.props.showOtherInsteafOfNumber}
/>
)}
</div>
Expand Down
54 changes: 54 additions & 0 deletions packages/lib/src/components/PayByBankUS/PayByBankUS.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@import '../../style/index';

.adyen-checkout-paybybank_AIS_DD {
margin-bottom: 16px;

&__description-header {
margin: 0 0 4px;
font-size: 16px;
font-weight: 500;
color: $color-black;
}

&__description-body {
font-weight: 400;
list-style-type: disc;
color: $color-gray-darker;
margin: 0;
font-size: 14px;
line-height: 1.5;
}
}

// apply the rule to the main dropin intem
.adyen-checkout__payment-method--paybybank_AIS_DD {
.adyen-checkout__payment-method__brands {
// a bit hacky but makes constum breakpoints to hide each set of images
@media (max-width: 330px) {
display: none;
}

@media (max-width: 360px) {
.adyen-checkout__payment-method__image__wrapper:nth-child(2) {
display: none;
}
}

@media (max-width: 390px) {
.adyen-checkout__payment-method__image__wrapper:nth-child(3) {
display: none;
}
}

@media (max-width: 420px) {
.adyen-checkout__payment-method__image__wrapper:nth-child(4) {
display: none;
}
}
}

.adyen-checkout__payment-method__brand-number {
text-overflow: clip;
white-space: nowrap;
}
}
102 changes: 102 additions & 0 deletions packages/lib/src/components/PayByBankUS/PayByBankUS.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { render, screen } from '@testing-library/preact';
import PayByBankUS from './PayByBankUS';
import userEvent from '@testing-library/user-event';

describe('PayByBank US', () => {
let onSubmitMock;
let user;

const mockSendAnalytics = jest.fn();
const analytics = {
sendAnalytics: mockSendAnalytics
};

beforeEach(() => {
onSubmitMock = jest.fn();
user = userEvent.setup();
});

test('should render payment description by default', async () => {
const pbb = new PayByBankUS({
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources, analytics }
});

render(pbb.render());
expect(await screen.findByText(/Use Pay by Bank to pay/i)).toBeTruthy();
expect(await screen.findByText(/By connecting your bank account/i)).toBeTruthy();
});

test('should render redirect button by default', async () => {
const pbb = new PayByBankUS({
onSubmit: onSubmitMock,
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources, analytics }
});

render(pbb.render());
const button = await screen.findByRole('button');
expect(button).toHaveTextContent('Continue to');

// check if button actually triggers submit
await user.click(button);
expect(onSubmitMock).toHaveBeenCalledTimes(1);
});

test('should not render pay button if showPayButton is false', () => {
const pbb = new PayByBankUS({
onSubmit: onSubmitMock,
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources, analytics },
showPayButton: false
});

render(pbb.render());
expect(screen.queryByRole('button')).not.toBeInTheDocument();

// check if submit is still callables
pbb.submit();
expect(onSubmitMock).toHaveBeenCalledTimes(1);
});

test('should not show disclaimer if is stored payment method', () => {
const pbb = new PayByBankUS({
storedPaymentMethodId: 'MOCK_ID',
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources, analytics }
});

render(pbb.render());
expect(screen.queryByText(/Use Pay by Bank to pay/i)).not.toBeInTheDocument();
expect(screen.queryByText(/By connecting your bank account/i)).not.toBeInTheDocument();
});

test('should show payButton with label Pay... if is stored payment method', async () => {
const pbb = new PayByBankUS({
storedPaymentMethodId: 'MOCK_ID',
showPayButton: true,
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources, analytics }
});

render(pbb.render());
expect(await screen.findByText(/Pay/i)).toBeInTheDocument();
});

test('should use label instead of payment method name if stored payment', () => {
const pbb = new PayByBankUS({
storedPaymentMethodId: 'MOCK_ID',
label: 'Label mock',
i18n: global.i18n,
loadingContext: 'test',
modules: { resources: global.resources, analytics }
});

expect(pbb.displayName).toBe('Label mock');
});
});
101 changes: 101 additions & 0 deletions packages/lib/src/components/PayByBankUS/PayByBankUS.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Fragment, h } from 'preact';
import CoreProvider from '../../core/Context/CoreProvider';
import RedirectElement from '../Redirect';
import RedirectButton from '../internal/RedirectButton';
import './PayByBankUS.scss';
import getIssuerImageUrl from '../../utils/get-issuer-image';
import PayButton, { payAmountLabel } from '../internal/PayButton';

export default class PayByBankUS extends RedirectElement {
public static type = 'paybybank_AIS_DD';

public static defaultProps = {
type: PayByBankUS.type,
showPayButton: true,
// paymentMethodBrands configuration
keepBrandsVisible: true,
showOtherInsteafOfNumber: true
};

public formatData() {
return {
paymentMethod: {
type: this.type,
...(this.props.storedPaymentMethodId && {
storedPaymentMethodId: this.props.storedPaymentMethodId
})
},
browserInfo: this.browserInfo
};
}

get displayName() {
if (this.props.storedPaymentMethodId && this.props.label) {
return this.props.label;
}
return this.props.name;
}

get additionalInfo() {
return this.props.storedPaymentMethodId ? this.props.name : '';
}

/*
Hardcode US brands
*/
get brands(): { icon: string; name: string }[] {
const getImage = props => this.resources.getImage(props);
// paybybank_AIS_DD / tx_variant not used here since images are kept in paybybank subfolder
const getIssuerIcon = getIssuerImageUrl({}, 'paybybank', getImage);

// hardcoding
return [
{ icon: getIssuerIcon('US-1'), name: 'Wells Fargo' },
{ icon: getIssuerIcon('US-2'), name: 'Bank of America' },
{ icon: getIssuerIcon('US-3'), name: 'Chase' },
{ icon: getIssuerIcon('US-4'), name: 'Citi' }
];
}

render() {
return (
<CoreProvider i18n={this.props.i18n} loadingContext={this.props.loadingContext} resources={this.resources}>
{this.props.storedPaymentMethodId ? (
this.props.showPayButton && (
<PayButton
{...this.props}
classNameModifiers={['standalone']}
amount={this.props.amount}
label={payAmountLabel(this.props.i18n, this.props.amount)}
onClick={this.submit}
/>
)
) : (
<Fragment>
<div className="adyen-checkout-paybybank_AIS_DD">
<p className="adyen-checkout-paybybank_AIS_DD__description-header">
{this.props.i18n.get('payByBankAISDD.disclaimer.header')}
</p>
<p className="adyen-checkout-paybybank_AIS_DD__description-body">
{this.props.i18n.get('payByBankAISDD.disclaimer.body')}
</p>
</div>

{this.props.showPayButton && (
<RedirectButton
{...this.props}
showPayButton={this.props.showPayButton}
name={this.displayName}
onSubmit={this.submit}
payButton={this.payButton}
ref={ref => {
this.componentRef = ref;
}}
/>
)}
</Fragment>
)}
</CoreProvider>
);
}
}
1 change: 1 addition & 0 deletions packages/lib/src/components/PayByBankUS/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './PayByBankUS';
3 changes: 3 additions & 0 deletions packages/lib/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import Trustly from './Trustly';
import PayMe from './PayMe';
import OnlineBankingFI from './OnlineBankingFI';
import Riverty from './Riverty';
import PayByBankUS from './PayByBankUS';

/**
* Maps each component with a Component element.
Expand Down Expand Up @@ -194,6 +195,8 @@ const componentsMap = {
twint: Twint,
vipps: Vipps,
trustly: Trustly,
paybybank_AIS_DD: PayByBankUS,

/** Redirect */

/** Klarna */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function filterEcomStoredPaymentMethods(pm) {
return !!pm && !!pm.supportedShopperInteractions && pm.supportedShopperInteractions.includes('Ecommerce');
}

const supportedStoredPaymentMethods = ['scheme', 'blik', 'twint', 'ach', 'cashapp'];
const supportedStoredPaymentMethods = ['scheme', 'blik', 'twint', 'ach', 'cashapp', 'paybybank_AIS_DD'];

export function filterSupportedStoredPaymentMethods(pm) {
return !!pm && !!pm.type && supportedStoredPaymentMethods.includes(pm.type);
Expand Down
7 changes: 5 additions & 2 deletions packages/lib/src/language/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"creditCard.cvcField.title.optional": "رمز الأمان (اختياري)",
"issuerList.wallet.placeholder": "حدد محفظتك",
"privacyPolicy": "سياسة الخصوصية",
"afterPay.agreement": "أوافق على ٪ @ لشركة Riverty",
"afterPay.agreement": "أوافق على %@ لشركة Riverty",
"riverty.termsAndConditions": "أوافق على %#الشروط والأحكام%# العامة لوسيلة دفع Riverty. يمكن العثور على سياسة الخصوصية لـ Riverty %#هنا%#.",
"paymentConditions": "شروط الدفع",
"openApp": "فتح التطبيق",
Expand Down Expand Up @@ -318,5 +318,8 @@
"payme.scanQrCode": "أكمل الدفع باستخدام رمز الاستجابة السريعة",
"payme.timeToPay": "رمز الاستجابة السريعة هذا صالح لـ %@",
"payme.instructions.steps": "افتح تطبيق PayMe. %@امسح رمز الاستجابة السريعة ضوئيًا للإذن بالدفع. %@أكمل عملية الدفع في التطبيق وانتظر التأكيد.",
"payme.instructions.footnote": "يرجى عدم إغلاق هذه الصفحة قبل إتمام الدفع"
"payme.instructions.footnote": "يرجى عدم إغلاق هذه الصفحة قبل إتمام الدفع",
"payByBankAISDD.disclaimer.header": "استخدم خدمة Pay by Bank للدفع الفوري من خلال أي حساب مصرفي.",
"payByBankAISDD.disclaimer.body": "من خلال ربط حسابك البنكي، أنت بذلك تمنحنا تفويضًا بخصم مدفوعات المبالغ المستحقة من حسابك البنكي، وذلك مقابل استخدام خدماتنا و/أو شراء منتجاتنا. ويستمر العمل بهذا التفويض إلى أن يتم إلغاؤه.",
"paymentMethodBrand.other": "أخرى"
}
Loading

0 comments on commit dec2404

Please sign in to comment.