Skip to content

Commit

Permalink
feat(payment): INT-2181 Add secured fields to support TSV with hosted…
Browse files Browse the repository at this point in the history
… fields
  • Loading branch information
Victor Parra committed Dec 31, 2019
1 parent 581eb7c commit 69d4753
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 45 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"homepage": "https://github.com/bigcommerce/checkout-js#readme",
"dependencies": {
"@bigcommerce/checkout-sdk": "^1.46.1",
"@bigcommerce/checkout-sdk": "^1.47.0",
"@bigcommerce/citadel": "^2.15.1",
"@bigcommerce/form-poster": "^1.2.2",
"@bigcommerce/memoize": "^1.0.0",
Expand Down
30 changes: 30 additions & 0 deletions src/app/payment/paymentMethod/AdyenV2CardValidation.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { mount } from 'enzyme';
import React, { FunctionComponent } from 'react';

import AdyenV2CardValidation, { AdyenV2CardValidationProps } from './AdyenV2CardValidation';

describe('AdyenV2CardValidation', () => {
let defaultProps: AdyenV2CardValidationProps;
let AdyenV2CardValidationTest: FunctionComponent<AdyenV2CardValidationProps>;

beforeEach(() => {
defaultProps = {
verificationFieldsContainerId: 'container',
shouldShowNumberField: true,
};

AdyenV2CardValidationTest = props => (
<AdyenV2CardValidation { ...props } />
);
});

it('renders Adyen V2 secured fields', () => {
const container = mount(<AdyenV2CardValidationTest { ...defaultProps } />);

expect(container.props())
.toEqual(expect.objectContaining({
verificationFieldsContainerId: 'container',
shouldShowNumberField: true,
}));
});
});
51 changes: 51 additions & 0 deletions src/app/payment/paymentMethod/AdyenV2CardValidation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import classNames from 'classnames';
import React from 'react';

import { TranslatedString } from '../../locale';

export interface AdyenV2CardValidationProps {
verificationFieldsContainerId?: string;
shouldShowNumberField: boolean;
}

const AdyenV2CardValidation: React.FunctionComponent<AdyenV2CardValidationProps> = ({
verificationFieldsContainerId,
shouldShowNumberField,
}) => (
<div>
{ shouldShowNumberField && <p>
<strong>
<TranslatedString id="payment.instrument_trusted_shipping_address_title_text" />
</strong>

<br />

<TranslatedString id="payment.instrument_trusted_shipping_address_text" />
</p> }

<div className="form-ccFields" id={ verificationFieldsContainerId }>
{ <div className="form-field form-field--ccNumber" style={ { display: (shouldShowNumberField) ? undefined : 'none' } }>
<label htmlFor="encryptedCardNumber">
<TranslatedString id="payment.credit_card_number_label" />
</label>
<div className="form-input optimizedCheckout-form-input has-icon" data-cse="encryptedCardNumber" id="encryptedCardNumber" />
</div> }
<div className="form-field form-ccFields-field--ccCvv">
<label htmlFor="encryptedSecurityCode">
<TranslatedString id="payment.credit_card_cvv_label" />
</label>
<div
className={ classNames(
'form-input',
'optimizedCheckout-form-input',
'has-icon'
) }
data-cse="encryptedSecurityCode"
id="encryptedSecurityCode"
/>
</div>
</div>
</div>
);

export default AdyenV2CardValidation;
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('when using Adyen V2 payment', () => {
expect(checkoutService.initializePayment)
.toHaveBeenCalledWith(expect.objectContaining({
adyenv2: {
cardVerificationContainerId: undefined,
containerId: 'scheme-adyen-component-field',
options: {
hasHolderName: true,
Expand Down
39 changes: 24 additions & 15 deletions src/app/payment/paymentMethod/AdyenV2PaymentMethod.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { AdyenCreditCardComponentOptions, PaymentInitializeOptions } from '@bigcommerce/checkout-sdk';
import { AdyenCreditCardComponentOptions } from '@bigcommerce/checkout-sdk';
import React, { createRef, useCallback, useRef, useState, FunctionComponent, RefObject } from 'react';
import { Omit } from 'utility-types';

import { TranslatedString } from '../../locale';
import { Modal } from '../../ui/modal';

import AdyenV2CardValidation from './AdyenV2CardValidation';
import HostedWidgetPaymentMethod, { HostedWidgetPaymentMethodProps } from './HostedWidgetPaymentMethod';

export type AdyenPaymentMethodProps = Omit<HostedWidgetPaymentMethodProps, 'containerId' | 'hideContentWhenSignedOut'>;
Expand Down Expand Up @@ -35,6 +36,7 @@ const AdyenV2PaymentMethod: FunctionComponent<AdyenPaymentMethodProps> = ({
const [threeDSecureContent, setThreeDSecureContent] = useState<HTMLElement>();
const containerId = `${method.id}-adyen-component-field`;
const threeDS2ContainerId = `${containerId}-3ds`;
const cardVerificationContainerId = `${method.id}-tsv`;
const component = method.id as AdyenMethodType;
const adyenOptions: AdyenOptions = {
[AdyenMethodType.scheme]: {
Expand Down Expand Up @@ -73,23 +75,29 @@ const AdyenV2PaymentMethod: FunctionComponent<AdyenPaymentMethodProps> = ({
}
}, []);

const adyenv2 = {
containerId,
threeDS2ContainerId,
options: adyenOptions[component],
threeDS2Options: {
widgetSize: '05',
onLoad,
onComplete,
},
};

const initializeAdyenPayment = useCallback((options: PaymentInitializeOptions) => {
const initializeAdyenPayment: HostedWidgetPaymentMethodProps['initializePayment'] = useCallback((options, selectedInstrumentId) => {
return initializePayment({
...options,
adyenv2,
adyenv2: {
cardVerificationContainerId: selectedInstrumentId && cardVerificationContainerId,
containerId,
options: adyenOptions[component],
threeDS2ContainerId,
threeDS2Options: {
widgetSize: '05',
onLoad,
onComplete,
},
},
});
}, [initializePayment, adyenv2]);
}, [initializePayment, component, cardVerificationContainerId, containerId, threeDS2ContainerId, adyenOptions, onLoad, onComplete]);

const validateInstrument = (shouldShowNumberField: boolean) => {
return <AdyenV2CardValidation
shouldShowNumberField={ shouldShowNumberField }
verificationFieldsContainerId={ cardVerificationContainerId }
/>;
};

return <>
<HostedWidgetPaymentMethod
Expand All @@ -98,6 +106,7 @@ const AdyenV2PaymentMethod: FunctionComponent<AdyenPaymentMethodProps> = ({
hideContentWhenSignedOut
initializePayment={ initializeAdyenPayment }
method={ method }
validateInstrument={ validateInstrument }
/>

<Modal
Expand Down
10 changes: 0 additions & 10 deletions src/app/payment/paymentMethod/HostedWidgetPaymentMethod.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,16 +265,6 @@ describe('HostedWidgetPaymentMethod', () => {
.toHaveLength(1);
});

it('does not show instruments fieldset when there are no stored instruments', () => {
jest.spyOn(checkoutState.data, 'getInstruments')
.mockReturnValue([]);

const component = mount(<HostedWidgetPaymentMethodTest { ...defaultProps } />);

expect(component.find(storedInstrumentModule.CardInstrumentFieldset))
.toHaveLength(0);
});

it('uses PaymentMethod to retrieve instruments', () => {
mount(<HostedWidgetPaymentMethodTest { ...defaultProps } />);

Expand Down
46 changes: 35 additions & 11 deletions src/app/payment/paymentMethod/HostedWidgetPaymentMethod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ export interface HostedWidgetPaymentMethodProps {
isUsingMultiShipping?: boolean;
isSignInRequired?: boolean;
method: PaymentMethod;
validateInstrument?(shouldShowNumberField: boolean): React.ReactNode;
deinitializeCustomer?(options: CustomerRequestOptions): Promise<CheckoutSelectors>;
deinitializePayment(options: PaymentRequestOptions): Promise<CheckoutSelectors>;
initializeCustomer?(options: CustomerInitializeOptions): Promise<CheckoutSelectors>;
initializePayment(options: PaymentInitializeOptions): Promise<CheckoutSelectors>;
initializePayment(options: PaymentInitializeOptions, selectedInstrumentId?: string): Promise<CheckoutSelectors>;
onPaymentSelect?(): void;
onSignOut?(): void;
onSignOutError?(error: Error): void;
Expand Down Expand Up @@ -121,13 +122,10 @@ class HostedWidgetPaymentMethod extends Component<
instruments,
containerId,
hideContentWhenSignedOut = false,
hideVerificationFields = false,
isInitializing = false,
isSignedIn = false,
isSignInRequired = false,
method,
isInstrumentCardCodeRequired: isInstrumentCardCodeRequiredProp,
isInstrumentCardNumberRequired: isInstrumentCardNumberRequiredProp,
isInstrumentFeatureAvailable: isInstrumentFeatureAvailableProp,
isLoadingInstruments,
} = this.props;
Expand All @@ -137,11 +135,9 @@ class HostedWidgetPaymentMethod extends Component<
selectedInstrumentId = this.getDefaultInstrumentId(),
} = this.state;

const selectedInstrument = find(instruments, { bigpayToken: selectedInstrumentId });
const shouldShowInstrumentFieldset = isInstrumentFeatureAvailableProp && instruments.length > 0;
const shouldShowCreditCardFieldset = !shouldShowInstrumentFieldset || isAddingNewCard;
const isLoading = isInitializing || isLoadingInstruments;
const shouldShowNumberField = selectedInstrument ? isInstrumentCardNumberRequiredProp(selectedInstrument) : false;

return (
<LoadingOverlay
Expand All @@ -153,10 +149,7 @@ class HostedWidgetPaymentMethod extends Component<
onSelectInstrument={ this.handleSelectInstrument }
onUseNewInstrument={ this.handleUseNewCard }
selectedInstrumentId={ selectedInstrumentId }
validateInstrument={ !hideVerificationFields && <CreditCardValidation
shouldShowCardCodeField={ isInstrumentCardCodeRequiredProp }
shouldShowNumberField={ shouldShowNumberField }
/> }
validateInstrument={ this.getValidateInstrument() }
/> }

<div
Expand All @@ -182,6 +175,35 @@ class HostedWidgetPaymentMethod extends Component<
);
}

getValidateInstrument(): ReactNode | undefined {
const {
hideVerificationFields,
instruments,
isInstrumentCardCodeRequired: isInstrumentCardCodeRequiredProp,
isInstrumentCardNumberRequired: isInstrumentCardNumberRequiredProp,
validateInstrument,
} = this.props;

const { selectedInstrumentId = this.getDefaultInstrumentId() } = this.state;
const selectedInstrument = find(instruments, { bigpayToken: selectedInstrumentId });
const shouldShowNumberField = selectedInstrument ? isInstrumentCardNumberRequiredProp(selectedInstrument) : false;

if (hideVerificationFields) {
return;
}

if (validateInstrument) {
return validateInstrument(shouldShowNumberField);
}

return (
<CreditCardValidation
shouldShowCardCodeField={ isInstrumentCardCodeRequiredProp }
shouldShowNumberField={ shouldShowNumberField }
/>
);
}

private async initializeMethod(): Promise<CheckoutSelectors | void> {
const {
isPaymentDataRequired,
Expand All @@ -194,6 +216,8 @@ class HostedWidgetPaymentMethod extends Component<
signInCustomer = noop,
} = this.props;

const { selectedInstrumentId = this.getDefaultInstrumentId() } = this.state;

if (!isPaymentDataRequired) {
setSubmit(method, null);

Expand All @@ -213,7 +237,7 @@ class HostedWidgetPaymentMethod extends Component<
return initializePayment({
gatewayId: method.gateway,
methodId: method.id,
});
}, selectedInstrumentId);
}

private getDefaultInstrumentId(): string | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,6 @@ describe('CardInstrumentFieldset', () => {
);

expect(component.find(ValidateInstrument).length)
.toEqual(0);
.toEqual(1);
});
});
4 changes: 3 additions & 1 deletion src/app/payment/storedInstrument/CardInstrumentFieldset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ const CardInstrumentFieldset: FunctionComponent<CardInstrumentFieldsetProps> = (
render={ renderInput }
/>

{ selectedInstrumentId && validateInstrument }
<div style={ {display: selectedInstrumentId ? undefined : 'none'} }>
{ validateInstrument }
</div>
</Fieldset>;
};

Expand Down

0 comments on commit 69d4753

Please sign in to comment.