Skip to content

Commit

Permalink
Checkout: adding Ebanx fields and validation to tef (#24240)
Browse files Browse the repository at this point in the history
* initial commit

* initial commit to be squashed, and revert `paymentMethods.byCountry[ 'BR' ]`

Adding validation - still WIP

Toying with further ebanx abstraction ( remove this commit after updating ebanx fields )

* Updated tests for TEF/Ebanx

* Updated tests for redirect payment box
Added snapshot test for select box
  • Loading branch information
ramonjd committed May 15, 2018
1 parent 4995f4b commit 22820c8
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 114 deletions.
24 changes: 24 additions & 0 deletions client/lib/checkout/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ export function creditCardFieldRules( additionalFieldRules = {} ) {
);
}

/**
* Returns the tef payment validation rule set
* See: client/my-sites/checkout/checkout/redirect-payment-box.jsx
* @returns {object} the ruleset
*/
export function tefPaymentFieldRules() {
return Object.assign(
{
name: {
description: i18n.translate( 'Your Name' ),
rules: [ 'required' ],
},

'tef-bank': {
description: i18n.translate( 'Bank' ),
rules: [ 'required' ],
},
},
ebanxFieldRules( 'BR' )
);
}

/**
* Returns a validation ruleset to use for the given payment type
* @param {object} paymentDetails object containing fieldname/value keypairs
Expand All @@ -73,6 +95,8 @@ export function paymentFieldRules( paymentDetails, paymentType ) {
switch ( paymentType ) {
case 'credit-card':
return creditCardFieldRules( getAdditionalFieldRules( paymentDetails ) );
case 'tef':
return tefPaymentFieldRules();
default:
return null;
}
Expand Down
261 changes: 150 additions & 111 deletions client/my-sites/checkout/checkout/redirect-payment-box.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
/**
* External dependencies
*/
import React, { PureComponent } from 'react';
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import { assign, snakeCase, some, map } from 'lodash';
import { snakeCase, some, map, zipObject, isEmpty } from 'lodash';

/**
* Internal dependencies
Expand All @@ -15,7 +15,7 @@ import CartCoupon from 'my-sites/checkout/cart/cart-coupon';
import PaymentChatButton from './payment-chat-button';
import CartToggle from './cart-toggle';
import TermsOfService from './terms-of-service';
import Input from 'my-sites/domains/components/form/input';
import { Input, Select } from 'my-sites/domains/components/form';
import cartValues, {
paymentMethodName,
paymentMethodClassName,
Expand All @@ -25,34 +25,88 @@ import SubscriptionText from './subscription-text';
import analytics from 'lib/analytics';
import wpcom from 'lib/wp';
import notices from 'notices';
import FormSelect from 'components/forms/form-select';
import FormLabel from 'components/forms/form-label';
import EbanxPaymentFields from 'my-sites/checkout/checkout/ebanx-payment-fields';
import { planMatches } from 'lib/plans';
import { TYPE_BUSINESS, GROUP_WPCOM } from 'lib/plans/constants';
import { validatePaymentDetails } from 'lib/checkout';
import { PAYMENT_PROCESSOR_EBANX_COUNTRIES } from 'lib/checkout/constants';

export class RedirectPaymentBox extends PureComponent {
static displayName = 'RedirectPaymentBox';

static propTypes = {
paymentType: PropTypes.string.isRequired,
cart: PropTypes.object.isRequired,
countriesList: PropTypes.object.isRequired,
transaction: PropTypes.object.isRequired,
redirectTo: PropTypes.func.isRequired,
};

constructor() {
super();
this.redirectToPayment = this.redirectToPayment.bind( this );
this.handleChange = this.handleChange.bind( this );
}
eventFormName = 'Checkout Form';

handleChange( event ) {
const data = {};
data[ event.target.name ] = event.target.value;
constructor( props ) {
super( props );
this.state = {
errorMessages: [],
paymentDetails: this.setPaymentDetailsState( props.paymentType ),
};
}

this.setState( data );
setPaymentDetailsState( paymentType ) {
let paymentDetailsState = {};
switch ( paymentType ) {
case 'tef':
paymentDetailsState = {
'tef-bank': '',
...zipObject(
PAYMENT_PROCESSOR_EBANX_COUNTRIES.BR.fields,
map( PAYMENT_PROCESSOR_EBANX_COUNTRIES.BR.fields, () => '' )
),
};
}
return {
name: '',
...paymentDetailsState,
};
}

handleChange = event => this.updateFieldValues( event.target.name, event.target.value );

getErrorMessage = fieldName => this.state.errorMessages[ fieldName ];

getFieldValue = fieldName => this.state.paymentDetails[ fieldName ];

updateFieldValues = ( name, value ) => {
this.setState( {
paymentDetails: {
...this.state.paymentDetails,
[ name ]: value,
},
} );
};

createField = ( fieldName, componentClass, props ) => {
const errorMessage = this.getErrorMessage( fieldName ) || [];
return React.createElement(
componentClass,
Object.assign(
{},
{
additionalClasses: 'checkout__checkout-field',
eventFormName: this.props.eventFormName,
isError: ! isEmpty( errorMessage ),
errorMessage: errorMessage[ 0 ],
name: fieldName,
onBlur: this.handleChange,
onChange: this.handleChange,
value: this.getFieldValue( fieldName ),
autoComplete: 'off',
},
props
)
);
};

setSubmitState( submitState ) {
if ( submitState.error ) {
notices.error( submitState.error );
Expand All @@ -70,10 +124,20 @@ export class RedirectPaymentBox extends PureComponent {
return paymentMethodClassName( paymentType ) || 'WPCOM_Billing_Stripe_Source';
}

redirectToPayment( event ) {
redirectToPayment = event => {
const origin = getLocationOrigin( location );
event.preventDefault();

const validation = validatePaymentDetails( this.state.paymentDetails, this.props.paymentType );

this.setState( {
errorMessages: validation.errors,
} );

if ( ! isEmpty( validation.errors ) ) {
return;
}

this.setSubmitState( {
info: translate( 'Setting up your %(paymentProvider)s payment', {
args: { paymentProvider: this.getPaymentProviderName() },
Expand All @@ -90,7 +154,7 @@ export class RedirectPaymentBox extends PureComponent {
}

const dataForApi = {
payment: assign( {}, this.state, {
payment: Object.assign( {}, this.state.paymentDetails, {
paymentMethod: this.paymentMethodByType( this.props.paymentType ),
successUrl: origin + this.props.redirectTo(),
cancelUrl,
Expand All @@ -100,36 +164,32 @@ export class RedirectPaymentBox extends PureComponent {
};

// get the redirect URL from rest endpoint
wpcom.undocumented().transactions(
'POST',
dataForApi,
function( error, result ) {
let errorMessage;
if ( error ) {
if ( error.message ) {
errorMessage = error.message;
} else {
errorMessage = translate( "We've encountered a problem. Please try again later." );
}

this.setSubmitState( {
error: errorMessage,
disabled: false,
} );
} else if ( result.redirect_url ) {
this.setSubmitState( {
info: translate( 'Redirecting you to the payment partner to complete the payment.' ),
disabled: true,
} );
analytics.ga.recordEvent( 'Upgrades', 'Clicked Checkout With Redirect Payment Button' );
analytics.tracks.recordEvent(
'calypso_checkout_with_redirect_' + snakeCase( this.props.paymentType )
);
location.href = result.redirect_url;
wpcom.undocumented().transactions( 'POST', dataForApi, ( error, result ) => {
let errorMessage;
if ( error ) {
if ( error.message ) {
errorMessage = error.message;
} else {
errorMessage = translate( "We've encountered a problem. Please try again later." );
}
}.bind( this )
);
}

this.setSubmitState( {
error: errorMessage,
disabled: false,
} );
} else if ( result.redirect_url ) {
this.setSubmitState( {
info: translate( 'Redirecting you to the payment partner to complete the payment.' ),
disabled: true,
} );
analytics.ga.recordEvent( 'Upgrades', 'Clicked Checkout With Redirect Payment Button' );
analytics.tracks.recordEvent(
'calypso_checkout_with_redirect_' + snakeCase( this.props.paymentType )
);
location.href = result.redirect_url;
}
} );
};

renderButtonText() {
if ( cartValues.cartItems.hasRenewalItem( this.props.cart ) ) {
Expand All @@ -149,77 +209,61 @@ export class RedirectPaymentBox extends PureComponent {
} );
}

renderBankOptions( paymentType ) {
getBankOptions( paymentType ) {
// Source https://stripe.com/docs/sources/ideal
const banks = {
ideal: {
abn_amro: 'ABN AMRO',
asn_bank: 'ASN Bank',
bunq: 'Bunq',
ing: 'ING',
knab: 'Knab',
rabobank: 'Rabobank',
regiobank: 'RegioBank',
sns_bank: 'SNS Bank',
triodos_bank: 'Triodos Bank',
van_lanschot: 'Van Lanschot',
},
tef: {
banrisul: 'Banrisul',
bradesco: 'Bradesco',
bancodobrasil: 'Banco do Brasil',
itau: 'Itaú',
},
ideal: [
{ value: 'abn_amro', label: 'ABN AMRO' },
{ value: 'asn_bank', label: 'ASN Bank' },
{ value: 'bunq', label: 'Bunq' },
{ value: 'ing', label: 'ING' },
{ value: 'knab', label: 'Knab' },
{ value: 'rabobank', label: 'Rabobank' },
{ value: 'regiobank', label: 'RegioBank' },
{ value: 'sns_bank', label: 'SNS Bank' },
{ value: 'triodos_bank', label: 'Triodos Bank' },
{ value: 'van_lanschot', label: 'Van Lanschot' },
],
tef: [
{ value: 'banrisul', label: 'Banrisul' },
{ value: 'bradesco', label: 'Bradesco' },
{ value: 'bancodobrasil', label: 'Banco do Brasil' },
{ value: 'itau', label: 'Itaú' },
],
};

const banksOptions = map( banks[ paymentType ], ( text, optionValue ) => (
<option value={ optionValue } key={ optionValue }>{ text }</option>
) );

return [
<option value="" key="-">{ translate( 'Please select your bank.' ) }</option>,
...banksOptions,
{ value: '', label: translate( 'Please select your bank.' ) },
...banks[ paymentType ],
];
}

renderAdditionalFields() {
switch ( this.props.paymentType ) {
case 'ideal':
return (
<div className="checkout__checkout-field">
<FormLabel htmlFor="ideal-bank">
{ translate( 'Bank' ) }
</FormLabel>
<FormSelect
name="ideal-bank"
onChange={ this.handleChange }
>
{ this.renderBankOptions( 'ideal' ) }
</FormSelect>
</div>
);
return this.createField( 'ideal-bank', Select, {
label: translate( 'Bank' ),
options: this.getBankOptions( 'ideal' ),
} );
case 'p24':
return (
<Input
additionalClasses="checkout-field"
name="email"
onChange={ this.handleChange }
label={ translate( 'Email Address' ) }
eventFormName="Checkout Form" />
);
return this.createField( 'email', Input, {
label: translate( 'Email Address' ),
} );
case 'tef':
return (
<div className="checkout__checkout-field">
<FormLabel htmlFor="tef-bank">
{ translate( 'Bank' ) }
</FormLabel>
<FormSelect
name="tef-bank"
onChange={ this.handleChange }
>
{ this.renderBankOptions( 'tef' ) }
</FormSelect>
</div>
<Fragment>
{ this.createField( 'tef-bank', Select, {
label: translate( 'Bank' ),
options: this.getBankOptions( 'tef' ),
} ) }
<EbanxPaymentFields
countryCode="BR"
countriesList={ this.props.countriesList }
getErrorMessage={ this.getErrorMessage }
getFieldValue={ this.getFieldValue }
handleFieldChange={ this.updateFieldValues }
eventFormName={ this.eventFormName }
/>
</Fragment>
);

}
Expand All @@ -237,14 +281,9 @@ export class RedirectPaymentBox extends PureComponent {
return (
<form onSubmit={ this.redirectToPayment }>
<div className="checkout__payment-box-section">
<Input
additionalClasses="checkout-field"
name="name"
onChange={ this.handleChange }
label={ translate( 'Your Name' ) }
eventFormName="Checkout Form"
/>

{ this.createField( 'name', Input, {
label: translate( 'Your Name' ),
} ) }
{ this.renderAdditionalFields() }
</div>

Expand Down
1 change: 1 addition & 0 deletions client/my-sites/checkout/checkout/secure-payment-form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ const SecurePaymentForm = createReactClass( {
<RedirectPaymentBox
cart={ this.props.cart }
transaction={ this.props.transaction }
countriesList={ countriesListForPayments }
selectedSite={ this.props.selectedSite }
paymentType={ paymentType }
redirectTo={ this.props.redirectTo }
Expand Down
Loading

0 comments on commit 22820c8

Please sign in to comment.