Skip to content

Commit

Permalink
Instant Payments on Drop-in (#1354)
Browse files Browse the repository at this point in the history
* feat: draft instant payment

* feat: closing active payment methods + ui fixes

* feat: increased specificity apple pay appearence

* fix: apple pay specificity

* fix: clean up

* feat: test for uielement

* feat: translations
  • Loading branch information
ribeiroguilherme authored Nov 23, 2021
1 parent 3400496 commit 3db67ba
Show file tree
Hide file tree
Showing 53 changed files with 319 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
@supports (-webkit-appearance: -apple-pay-button) {

/*
* Combination of both classes improve the specificity, avoiding
* overwrite of the -webkit-appearence by the button native css
*/
.apple-pay,
.apple-pay-button {
display: inline-block;
-webkit-appearance: -apple-pay-button;
}
.apple-pay-button {
display: inline-block;
cursor: pointer;
}
.apple-pay-button-black {
Expand All @@ -13,7 +21,6 @@
.apple-pay-button-white-with-line {
-apple-pay-button-style: white-outline;
}

/* Apple Pay Button types https://developer.apple.com/documentation/apple_pay_on_the_web/displaying_apple_pay_buttons */
.apple-pay-button--type-plain {
-apple-pay-button-type: plain;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.adyen-checkout__applepay__button {
width: 240px;
height: 40px;
height: 48px;
}

.adyen-checkout__dropin .adyen-checkout__applepay__button {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ApplePayButton extends Component<ApplePayButtonProps> {
'adyen-checkout__applepay__button',
`adyen-checkout__applepay__button--${buttonColor}`,
`adyen-checkout__applepay__button--${buttonType}`,
[styles['apple-pay']],
[styles['apple-pay-button']],
[styles[`apple-pay-button-${buttonColor}`]],
[styles[`apple-pay-button--type-${buttonType}`]]
Expand Down
8 changes: 6 additions & 2 deletions packages/lib/src/components/ApplePay/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ export interface ApplePayElementProps extends UIElementProps {
*/
countryCode: string;

/**
* Part of the 'ApplePayLineItem' object, which sets the label of the payment request
* @see {@link https://developer.apple.com/documentation/apple_pay_on_the_web/applepaylineitem ApplePayLineItem docs}
*/
totalPriceLabel: string;

/**
* @default 'final'
*/
totalPriceStatus?: ApplePayJS.ApplePayLineItemType;

totalPriceLabel?: string;

configuration: {
merchantName?: string;
merchantId?: string;
Expand Down
4 changes: 2 additions & 2 deletions packages/lib/src/components/BaseElement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from 'preact';
import { ComponentChild, render } from 'preact';
import getProp from '../utils/getProp';
import EventEmitter from './EventEmitter';
import uuid from '../utils/uuid';
Expand Down Expand Up @@ -60,7 +60,7 @@ class BaseElement<P extends BaseElementProps> {
};
}

protected render() {
public render(): ComponentChild | Error {
// render() not implemented in the element
throw new Error('Payment method cannot be rendered.');
}
Expand Down
48 changes: 46 additions & 2 deletions packages/lib/src/components/Dropin/Dropin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Dropin', () => {

beforeEach(() => {
const checkout = new AdyenCheckout({});
dropin = checkout.create('dropin', {});
dropin = checkout.create('dropin');
});

describe('isValid', () => {
Expand Down Expand Up @@ -80,7 +80,7 @@ describe('Dropin', () => {
test('should handle new challenge action', () => {
const checkout = new AdyenCheckout({});

const dropin = checkout.create('dropin', {});
const dropin = checkout.create('dropin');

const pa = dropin.handleAction(challengeAction);
expect(pa.componentFromAction instanceof ThreeDS2Challenge).toEqual(true);
Expand Down Expand Up @@ -112,4 +112,48 @@ describe('Dropin', () => {
expect(pa.componentFromAction.props.challengeWindowSize).toEqual('03');
});
});

describe('Instant Payments feature', () => {
test('formatProps formats instantPaymentTypes removing duplicates and invalid values', () => {
const checkout = new AdyenCheckout({});
// @ts-ignore
const dropin = checkout.create('dropin', { instantPaymentTypes: ['alipay', 'paywithgoogle', 'paywithgoogle', 'paypal'] });

expect(dropin.props.instantPaymentTypes).toStrictEqual(['paywithgoogle']);
});

test('formatProps filter out instantPaymentMethods from paymentMethods list ', () => {
const checkout = new AdyenCheckout({
paymentMethodsResponse: {
paymentMethods: [
{ name: 'Google Pay', type: 'paywithgoogle' },
{ name: 'AliPay', type: 'alipay' }
]
}
});
const dropin = checkout.create('dropin', { instantPaymentTypes: ['paywithgoogle'] });

expect(dropin.props.paymentMethods).toHaveLength(1);
expect(dropin.props.paymentMethods[0]).toStrictEqual({ type: 'alipay', name: 'AliPay' });
expect(dropin.props.instantPaymentMethods).toHaveLength(1);
expect(dropin.props.instantPaymentMethods[0]).toStrictEqual({ name: 'Google Pay', type: 'paywithgoogle' });
});

test('formatProps does not change paymentMethods list if instantPaymentType is not provided', () => {
const paymentMethods = [
{ name: 'Google Pay', type: 'paywithgoogle' },
{ name: 'AliPay', type: 'alipay' }
];

const checkout = new AdyenCheckout({
paymentMethodsResponse: {
paymentMethods
}
});
const dropin = checkout.create('dropin');

expect(dropin.props.paymentMethods).toStrictEqual(paymentMethods);
expect(dropin.props.instantPaymentMethods).toHaveLength(0);
});
});
});
35 changes: 31 additions & 4 deletions packages/lib/src/components/Dropin/Dropin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import defaultProps from './defaultProps';
import DropinComponent from '../../components/Dropin/components/DropinComponent';
import CoreProvider from '../../core/Context/CoreProvider';
import { PaymentAction } from '../../types';
import { DropinElementProps } from './types';
import { DropinElementProps, InstantPaymentTypes } from './types';
import { getCommonProps } from './components/utils';
import { createElements, createStoredElements } from './elements';
import createInstantPaymentElements from './elements/createInstantPaymentElements';

const SUPPORTED_INSTANT_PAYMENTS = ['paywithgoogle', 'applepay'];

class DropinElement extends UIElement<DropinElementProps> {
public static type = 'dropin';
Expand All @@ -19,6 +22,27 @@ class DropinElement extends UIElement<DropinElementProps> {
this.handleAction = this.handleAction.bind(this);
}

formatProps(props) {
const instantPaymentTypes: InstantPaymentTypes[] = Array.from<InstantPaymentTypes>(new Set(props.instantPaymentTypes)).filter(value =>
SUPPORTED_INSTANT_PAYMENTS.includes(value)
);

const instantPaymentMethods = instantPaymentTypes.reduce((memo, paymentType) => {
const paymentMethod = props.paymentMethods.find(({ type }) => type === paymentType);
if (paymentMethod) return [...memo, paymentMethod];
return memo;
}, []);

const paymentMethods = props.paymentMethods.filter(({ type }) => !instantPaymentTypes.includes(type));

return {
...super.formatProps(props),
instantPaymentTypes,
instantPaymentMethods,
paymentMethods
};
}

get isValid() {
return !!this.dropinRef && !!this.dropinRef.state.activePaymentMethod && !!this.dropinRef.state.activePaymentMethod.isValid;
}
Expand Down Expand Up @@ -70,12 +94,15 @@ class DropinElement extends UIElement<DropinElementProps> {
* Creates the Drop-in elements
*/
private handleCreate = () => {
const { paymentMethods, storedPaymentMethods, showStoredPaymentMethods, showPaymentMethods } = this.props;
const commonProps = getCommonProps({ ...this.props, /*onSubmit: this.submit,*/ elementRef: this.elementRef });
const { paymentMethods, storedPaymentMethods, showStoredPaymentMethods, showPaymentMethods, instantPaymentMethods } = this.props;

const commonProps = getCommonProps({ ...this.props, elementRef: this.elementRef });

const storedElements = showStoredPaymentMethods ? createStoredElements(storedPaymentMethods, commonProps, this._parentInstance.create) : [];
const elements = showPaymentMethods ? createElements(paymentMethods, commonProps, this._parentInstance.create) : [];
const instantPaymentElements = createInstantPaymentElements(instantPaymentMethods, commonProps, this._parentInstance.create);

return [storedElements, elements];
return [storedElements, elements, instantPaymentElements];
};

handleAction(action: PaymentAction, props = {}) {
Expand Down
10 changes: 10 additions & 0 deletions packages/lib/src/components/Dropin/components/DropinComponent.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
pointer-events: none;
}

.adyen-checkout__instant-payment-methods-list {
list-style: none;
margin: 0;
padding: 0;

li:not(:last-child) {
margin-bottom: 8px;
}
}

/* Forms */

.adyen-checkout__link {
Expand Down
32 changes: 18 additions & 14 deletions packages/lib/src/components/Dropin/components/DropinComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import './DropinComponent.scss';
export class DropinComponent extends Component<DropinComponentProps, DropinComponentState> {
public state: DropinComponentState = {
elements: [],
instantPaymentElements: [],
orderStatus: null,
isDisabling: false,
status: { type: 'loading' },
Expand All @@ -21,22 +22,24 @@ export class DropinComponent extends Component<DropinComponentProps, DropinCompo

public prepareDropinData = () => {
const { order, clientKey, loadingContext } = this.props;
const [storedElementsPromises, elementsPromises] = this.props.onCreateElements();
const [storedElementsPromises, elementsPromises, instantPaymentsPromises] = this.props.onCreateElements();
const orderStatusPromise = order ? getOrderStatus({ clientKey, loadingContext }, order) : null;

Promise.all([storedElementsPromises, elementsPromises, orderStatusPromise]).then(([storedElements, elements, orderStatus]) => {
this.setState({ elements: [...storedElements, ...elements], orderStatus });
this.setStatus({ type: 'ready' });

if (this.props.modules.analytics) {
this.props.modules.analytics.send({
containerWidth: this.base && (this.base as HTMLElement).offsetWidth,
paymentMethods: elements.map(e => e.props.type),
component: 'dropin',
flavor: 'dropin'
});
Promise.all([storedElementsPromises, elementsPromises, instantPaymentsPromises, orderStatusPromise]).then(
([storedElements, elements, instantPaymentElements, orderStatus]) => {
this.setState({ instantPaymentElements, elements: [...storedElements, ...elements], orderStatus });
this.setStatus({ type: 'ready' });

if (this.props.modules.analytics) {
this.props.modules.analytics.send({
containerWidth: this.base && (this.base as HTMLElement).offsetWidth,
paymentMethods: elements.map(e => e.props.type),
component: 'dropin',
flavor: 'dropin'
});
}
}
});
);
};

private setStatus = status => {
Expand Down Expand Up @@ -88,7 +91,7 @@ export class DropinComponent extends Component<DropinComponentProps, DropinCompo
this.setState({ activePaymentMethod: null });
}

render(props, { elements, status, activePaymentMethod, cachedPaymentMethods }) {
render(props, { elements, instantPaymentElements, status, activePaymentMethod, cachedPaymentMethods }) {
const isLoading = status.type === 'loading';
const isRedirecting = status.type === 'redirect';

Expand All @@ -112,6 +115,7 @@ export class DropinComponent extends Component<DropinComponentProps, DropinCompo
isLoading={isLoading || isRedirecting}
isDisabling={this.state.isDisabling}
paymentMethods={elements}
instantPaymentMethods={instantPaymentElements}
activePaymentMethod={activePaymentMethod}
cachedPaymentMethods={cachedPaymentMethods}
order={this.props.order}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Fragment, h } from 'preact';
import ContentSeparator from '../../../internal/ContentSeparator';
import useCoreContext from '../../../../core/Context/useCoreContext';
import UIElement from '../../../UIElement';

interface InstantPaymentMethodsProps {
paymentMethods: UIElement[];
}

function InstantPaymentMethods({ paymentMethods }: InstantPaymentMethodsProps) {
const { i18n } = useCoreContext();

return (
<Fragment>
<ul className="adyen-checkout__instant-payment-methods-list">
{paymentMethods.map(pm => (
<li key={pm.type}>{pm.render()}</li>
))}
</ul>
<ContentSeparator label={i18n.get('orPayWith')} />
</Fragment>
);
}

export default InstantPaymentMethods;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { shallow, mount } from 'enzyme';
import { h } from 'preact';
import PaymentMethodList from './PaymentMethodList';
import PaymentMethodItem from './PaymentMethodItem';
import InstantPaymentMethods from './InstantPaymentMethods';

const i18n = { get: key => key };
const index = 0;
Expand All @@ -24,11 +25,21 @@ const paymentMethods = [
}
];

const instantPaymentMethods = [
{
props: {
id: '3',
type: 'googlepay'
}
}
];

describe('PaymentMethodList', () => {
const getWrapper = props => shallow(<PaymentMethodList i18n={i18n} {...props} />);

test('Renders a PaymentMethodList', () => {
const wrapper = getWrapper({ paymentMethods });

expect(wrapper.hasClass('adyen-checkout__payment-methods-list')).toBe(true);
expect(wrapper.find(PaymentMethodItem).length).toBe(2);
});
Expand All @@ -55,4 +66,9 @@ describe('PaymentMethodList', () => {
getWrapper({ paymentMethods, onSelect, openFirstStoredPaymentMethod: true });
expect(onSelect.mock.calls.length).toBe(0);
});

test('Renders InstantPaymentMethods when prop is provided', () => {
const wrapper = getWrapper({ paymentMethods, instantPaymentMethods });
expect(wrapper.find(InstantPaymentMethods)).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import styles from '../DropinComponent.module.scss';
import UIElement from '../../../UIElement';
import { Order, OrderStatus } from '../../../../types';
import OrderPaymentMethods from './OrderPaymentMethods';
import InstantPaymentMethods from './InstantPaymentMethods';

interface PaymentMethodListProps {
paymentMethods: UIElement[];
instantPaymentMethods: UIElement[];
activePaymentMethod: UIElement;
cachedPaymentMethods: object;
order?: Order;
Expand All @@ -27,6 +29,7 @@ interface PaymentMethodListProps {

class PaymentMethodList extends Component<PaymentMethodListProps> {
public static defaultProps: PaymentMethodListProps = {
instantPaymentMethods: [],
paymentMethods: [],
activePaymentMethod: null,
cachedPaymentMethods: {},
Expand All @@ -52,7 +55,7 @@ class PaymentMethodList extends Component<PaymentMethodListProps> {

public onSelect = paymentMethod => () => this.props.onSelect(paymentMethod);

render({ paymentMethods, activePaymentMethod, cachedPaymentMethods, isLoading }) {
render({ paymentMethods, instantPaymentMethods, activePaymentMethod, cachedPaymentMethods, isLoading }) {
const paymentMethodListClassnames = classNames({
[styles['adyen-checkout__payment-methods-list']]: true,
'adyen-checkout__payment-methods-list': true,
Expand All @@ -65,6 +68,8 @@ class PaymentMethodList extends Component<PaymentMethodListProps> {
<OrderPaymentMethods order={this.props.order} orderStatus={this.props.orderStatus} onOrderCancel={this.props.onOrderCancel} />
)}

{!!instantPaymentMethods.length && <InstantPaymentMethods paymentMethods={instantPaymentMethods} />}

<ul className={paymentMethodListClassnames}>
{paymentMethods.map((paymentMethod, index, paymentMethodsCollection) => {
const isSelected = activePaymentMethod && activePaymentMethod._id === paymentMethod._id;
Expand Down
4 changes: 1 addition & 3 deletions packages/lib/src/components/Dropin/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ export default {
onSelect: () => {}, // triggered when a paymentMethod is selected
onDisableStoredPaymentMethod: null, // triggered when a shopper removes a storedPaymentMethod
onChange: () => {},
// onSubmit: () => {},
// onAdditionalDetails: () => {},

instantPaymentMethods: [],
amount: {},
installmentOptions: {},
paymentMethodsConfiguration: {}, // per paymentMethod configuration
Expand Down
Loading

0 comments on commit 3db67ba

Please sign in to comment.