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

Bacs Direct Debit component #568

Merged
merged 56 commits into from
Dec 10, 2020
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
89d7bdf
BacsDD - first draft
sponglord Nov 30, 2020
9e71d49
Validating holderName, bankAccount number & shopperEmail
sponglord Nov 30, 2020
30d867d
Added bankLocationId field
sponglord Nov 30, 2020
fa81b8a
Added checkbox validation
sponglord Nov 30, 2020
99b48c7
Added second 'confirm' state, class names and aria attributes
sponglord Dec 1, 2020
f2e0331
Added 'Edit' button to confirm page
sponglord Dec 1, 2020
2480619
Hide valid ticks on confirm page
sponglord Dec 1, 2020
4db505a
Fixed validation rules for email and name (return object is same as f…
sponglord Dec 2, 2020
281d966
BacsDD calls setStatus on BacsInput to trigger re-render
sponglord Dec 2, 2020
eb4f699
Added prop to interface
sponglord Dec 2, 2020
627e0d8
BacsResult (voucher) - first draft
sponglord Dec 2, 2020
c0753d7
Adjusted styling on result
sponglord Dec 3, 2020
2b0b869
Latest files so we can make a Draft PR
sponglord Dec 3, 2020
f5c477e
Adjusted input widths for bankAccount and locationId
sponglord Dec 3, 2020
b183aef
Removed billingAddress from /payments call now that API has been updated
sponglord Dec 3, 2020
00214d8
Added lock icon and amount to pay button
sponglord Dec 3, 2020
08b2700
BacsDD removing preSubmit function & own PayButton
sponglord Dec 4, 2020
b03cb43
BacsDD cleaning up state & status change mechanism
sponglord Dec 4, 2020
55f8970
BacsDD cleaning up render method
sponglord Dec 4, 2020
6dac11b
changed error msg text
sponglord Dec 4, 2020
b7f3402
Internalising state into the BacsInput comp
sponglord Dec 4, 2020
3c7725e
Removed checks for undefined status
sponglord Dec 4, 2020
e2b303f
Add Bacs to the vouchers playground
sponglord Dec 4, 2020
50356ca
BacsInput uses hook to store local isValid state
sponglord Dec 4, 2020
49dace4
removed unnecessary return
sponglord Dec 4, 2020
aeabc93
Added possibility to specify text on Voucher download button
sponglord Dec 4, 2020
6418160
Fixing CI error
sponglord Dec 4, 2020
197b989
Fix ConsentCheckbox on Bacs Component
marcperez Dec 4, 2020
67a1d3a
Merge branch 'feature/BacsDD' of github.com:Adyen/adyen-web into feat…
marcperez Dec 4, 2020
f71c65a
Fixing CI error
sponglord Dec 4, 2020
a4d6ec8
Added translation keys
sponglord Dec 8, 2020
d839189
Merge branch 'master' into feature/BacsDD
sponglord Dec 8, 2020
68fb5f0
Add bacs to components playground
sponglord Dec 8, 2020
9a1418a
Merge branch 'master' into feature/BacsDD
sponglord Dec 8, 2020
b3f2932
Merging changes from master
sponglord Dec 8, 2020
56d68cb
Can now make payment when component is standalone
sponglord Dec 8, 2020
c9136d4
Removed BacsResult.scss (not needed after merge from master)
sponglord Dec 8, 2020
e5ae213
Restoring BacsInput.scss and removing BacsResult.scss
sponglord Dec 8, 2020
81a9977
Update packages/lib/src/components/BacsDD/components/BacsInput.scss
sponglord Dec 9, 2020
d37466f
Update packages/lib/src/components/BacsDD/components/BacsInput.scss
sponglord Dec 9, 2020
1e5fe0f
Update packages/lib/src/components/BacsDD/components/BacsInput.scss
sponglord Dec 9, 2020
c38b601
Update packages/lib/src/components/BacsDD/components/BacsInput.scss
sponglord Dec 9, 2020
1e43b3a
Update packages/lib/src/components/BacsDD/components/BacsInput.scss
sponglord Dec 9, 2020
73f8bda
Update packages/lib/src/components/BacsDD/components/BacsInput.scss
sponglord Dec 9, 2020
7cb8798
Restoring BacsResult.scss and using .scss vars
sponglord Dec 9, 2020
d57d005
Merge remote-tracking branch 'origin/feature/BacsDD' into feature/BacsDD
sponglord Dec 9, 2020
b7c4151
Restoring BacsResult.scss and using .scss vars (post merge)
sponglord Dec 9, 2020
a5f79d2
Update packages/lib/src/components/BacsDD/components/BacsInput.scss
sponglord Dec 9, 2020
cd2f382
Fixed UI flash when Pay pressed. The form, in an editable state, was …
sponglord Dec 9, 2020
e9fa1c3
Merge remote-tracking branch 'origin/feature/BacsDD' into feature/BacsDD
sponglord Dec 9, 2020
e8e6304
BacsDD.tsx overrides UIElement.payButton
sponglord Dec 9, 2020
704260d
Restoring original ThreeDS2Challenge.tsx error message
sponglord Dec 9, 2020
facfaf8
Adding bacsResult to Voucher playground
sponglord Dec 9, 2020
d420e36
First tests for BacsInput.tsx
sponglord Dec 9, 2020
2d028a6
First tests for BacsInput.tsx (added to PR)
sponglord Dec 10, 2020
6efb177
Removed unused imports
sponglord Dec 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 64 additions & 0 deletions packages/lib/src/components/BacsDD/BacsDD.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { h } from 'preact';
import UIElement from '../UIElement';
import BacsInput from './components/BacsInput';
import CoreProvider from '../../core/Context/CoreProvider';
import BacsResult from './components/BacsResult';

interface BacsElementData {
paymentMethod: {
type: string;
holderName: string;
bankAccountNumber: string;
bankLocationId: string;
};
shopperEmail: string;
}

class BacsElement extends UIElement {
public static type = 'directdebit_GB';

formatData(): BacsElementData {
return {
paymentMethod: {
type: BacsElement.type,
...(this.state.data?.holderName && { holderName: this.state.data.holderName }),
...(this.state.data?.bankAccountNumber && { bankAccountNumber: this.state.data.bankAccountNumber }),
...(this.state.data?.bankLocationId && { bankLocationId: this.state.data.bankLocationId })
},
...(this.state.data?.shopperEmail && { shopperEmail: this.state.data.shopperEmail })
};
}

get isValid(): boolean {
return !!this.state.isValid;
}

render() {
return (
<CoreProvider i18n={this.props.i18n} loadingContext={this.props.loadingContext}>
{this.props.url ? (
<BacsResult
ref={ref => {
this.componentRef = ref;
}}
icon={this.icon}
url={this.props.url}
paymentMethodType={this.props.paymentMethodType}
/>
) : (
<BacsInput
ref={ref => {
this.componentRef = ref;
}}
{...this.props}
onChange={this.setState}
payButton={this.payButton}
onSubmit={this.submit}
/>
)}
</CoreProvider>
);
}
}

export default BacsElement;
41 changes: 41 additions & 0 deletions packages/lib/src/components/BacsDD/components/BacsInput.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@import '../../../style/index';

.adyen-checkout__bacs{

&--confirm {
position: relative;

.adyen-checkout-input__inline-validation--valid {
display: none
}
}

.adyen-checkout__field {

&--inactive {
pointer-events: none;
}
}

.adyen-checkout__bacs--edit {

position: absolute;
top: -25px;
right: 0px;
cursor: pointer;
width: 20%;

&-dropin{
top: -50px;
}

.adyen-checkout__bacs--edit-button {
border: none;
background: none;
color: $color-blue;
text-decoration: underline;
text-align: end;
cursor: pointer;
}
}
}
278 changes: 278 additions & 0 deletions packages/lib/src/components/BacsDD/components/BacsInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import classNames from 'classnames';
import useCoreContext from '../../../core/Context/useCoreContext';
import Field from '../../internal/FormFields/Field';
import { renderFormField } from '../../internal/FormFields';
import ConsentCheckbox from '../../internal/FormFields/ConsentCheckbox';
import { bacsValidationRules } from './validate';
import Validator from '../../../utils/Validator';
import { BacsDataState, BacsErrorsState, BacsInputProps, BacsValidState, ValidationObject } from './types';
import './BacsInput.scss';
import getImage from '../../../utils/get-image';

const ENTER_STATE = 'enter-data';
const CONFIRM_STATE = 'confirm-data';

function BacsInput(props: BacsInputProps) {
const { i18n } = useCoreContext();
const validator = new Validator(bacsValidationRules);

const [status, setStatus] = useState(ENTER_STATE);
this.setStatus = setStatus;

const [data, setData] = useState<BacsDataState>(props.data);
const [errors, setErrors] = useState<BacsErrorsState>({});
const [valid, setValid] = useState<BacsValidState>({
...(props.data.holderName && {
holderName: validator.validate('holderName', 'input')(props.data.holderName).isValid
}),
...(props.data.bankAccountNumber && {
bankAccountNumber: validator.validate('bankAccountNumber', 'input')(props.data.bankAccountNumber).isValid
}),
...(props.data.bankLocationId && {
bankLocationId: validator.validate('bankLocationId', 'input')(props.data.bankLocationId).isValid
}),
...(props.data.shopperEmail && {
shopperEmail: validator.validate('shopperEmail', 'input')(props.data.shopperEmail).isValid
})
});

const [isValid, setIsValid] = useState(false);

this.showValidation = (): void => {
setErrors({
holderName: !validator.validate('holderName', 'blur')(data.holderName).isValid,
bankAccountNumber: !validator.validate('bankAccountNumber', 'blur')(data.bankAccountNumber).isValid,
bankLocationId: !validator.validate('bankLocationId', 'blur')(data.bankLocationId).isValid,
shopperEmail: !validator.validate('shopperEmail', 'blur')(data.shopperEmail).isValid,
amountConsentCheckbox: !data.amountConsentCheckbox,
accountConsentCheckbox: !data.accountConsentCheckbox
});
};

const handleEventFor = (key: string, mode: string) => (e: Event): void => {
const val: string = (e.target as HTMLInputElement).value;
const { value, isValid, showError }: ValidationObject = validator.validate(key, mode)(val);

setData({ ...data, [key]: value });
setErrors({ ...errors, [key]: !isValid && showError });
setValid({ ...valid, [key]: isValid });
};

const handleConsentCheckbox = (key: string) => (): void => {
const checked = !data[key];
setData(prevData => ({ ...prevData, [key]: checked }));
setValid(prevValid => ({ ...prevValid, [key]: checked }));
setErrors(prevErrors => ({ ...prevErrors, [key]: !checked }));
};

const handlePayButton = () => {
if (!isValid) {
this.showValidation();
return false;
}

if (status === ENTER_STATE) {
this.setStatus(CONFIRM_STATE);
return;
}

if (status === CONFIRM_STATE) {
props.onSubmit();
}
};

const handleEdit = () => {
this.setStatus(ENTER_STATE);
return;
};

useEffect(() => {
const pmIsValid =
valid.holderName &&
valid.bankAccountNumber &&
valid.bankLocationId &&
valid.shopperEmail &&
!!valid.amountConsentCheckbox &&
!!valid.accountConsentCheckbox;

setIsValid(pmIsValid);

props.onChange({
data,
isValid: pmIsValid
});
}, [data, valid]);

return (
<div
className={classNames({
'adyen-checkout__bacs': true,
'adyen-checkout__bacs--confirm': status === CONFIRM_STATE || status === 'loading'
})}
>
{status == CONFIRM_STATE && (
<div
className={classNames({
'adyen-checkout__bacs--edit': true,
'adyen-checkout__bacs--edit-dropin': props.isDropin
})}
>
{renderFormField('text', {
name: 'bacsEdit',
className: 'adyen-checkout__bacs--edit-button',
value: i18n.get('edit'),
'aria-label': i18n.get('edit'),
readonly: true,
onClick: handleEdit
})}
</div>
)}

<Field
className={classNames({
'adyen-checkout__bacs--holder-name': true,
'adyen-checkout__field--inactive': status === CONFIRM_STATE || status === 'loading'
})}
label={i18n.get('bacs.accountHolderName')}
errorMessage={errors.holderName ? i18n.get('bacs.accountHolderName.invalid') : false}
isValid={valid.holderName}
>
{renderFormField('text', {
name: 'bacs.accountHolderName',
className: 'adyen-checkout__bacs-input--holder-name',
placeholder: props.placeholders.holderName,
value: data.holderName,
'aria-invalid': !valid.holderName,
'aria-label': i18n.get('bacs.accountHolderName'),
'aria-required': 'true',
required: true,
readonly: status === CONFIRM_STATE || status === 'loading',
autocorrect: 'off',
onChange: handleEventFor('holderName', 'blur'),
onInput: handleEventFor('holderName', 'input')
})}
</Field>

<div className="adyen-checkout__bacs__num-id adyen-checkout__field-wrapper">
<Field
errorMessage={!!errors.bankAccountNumber && i18n.get('bacs.accountNumber.invalid')}
label={i18n.get('bacs.accountNumber')}
className={classNames({
'adyen-checkout__bacs--bank-account-number': true,
'adyen-checkout__field--inactive': status === CONFIRM_STATE || status === 'loading'
})}
classNameModifiers={['col-70']}
isValid={valid.bankAccountNumber}
>
{renderFormField('text', {
value: data.bankAccountNumber,
className: 'adyen-checkout__bacs-input--bank-account-number',
placeholder: props.placeholders.bankAccountNumber,
'aria-invalid': !valid.bankAccountNumber,
'aria-label': i18n.get('bacs.accountNumber'),
'aria-required': 'true',
required: true,
readonly: status === CONFIRM_STATE || status === 'loading',
autocorrect: 'off',
onChange: handleEventFor('bankAccountNumber', 'blur'),
onInput: handleEventFor('bankAccountNumber', 'input')
})}
</Field>

<Field
errorMessage={!!errors.bankLocationId && i18n.get('bacs.bankLocationId.invalid')}
label={i18n.get('bacs.bankLocationId')}
className={classNames({
'adyen-checkout__bacs--bank-location-id': true,
'adyen-checkout__field--inactive': status === CONFIRM_STATE || status === 'loading'
})}
classNameModifiers={['col-30']}
isValid={valid.bankLocationId}
>
{renderFormField('text', {
value: data.bankLocationId,
className: 'adyen-checkout__bacs-input--bank-location-id',
placeholder: props.placeholders.bankLocationId,
'aria-invalid': !valid.bankLocationId,
'aria-label': i18n.get('bacs.bankLocationId'),
'aria-required': 'true',
required: true,
readonly: status === CONFIRM_STATE || status === 'loading',
autocorrect: 'off',
onChange: handleEventFor('bankLocationId', 'blur'),
onInput: handleEventFor('bankLocationId', 'input')
})}
</Field>
</div>

<Field
errorMessage={!!errors.shopperEmail && i18n.get('bacs.shopperEmail.invalid')}
label={i18n.get('bacs.shopperEmail')}
className={classNames({
'adyen-checkout__bacs--shopper-email': true,
'adyen-checkout__field--inactive': status === CONFIRM_STATE || status === 'loading'
})}
isValid={valid.shopperEmail}
>
{renderFormField('emailAddress', {
value: data.shopperEmail,
name: 'shopperEmail',
className: 'adyen-checkout__bacs-input--shopper-email',
classNameModifiers: ['large'],
placeholder: props.placeholders.shopperEmail,
spellcheck: false,
'aria-invalid': !valid.shopperEmail,
'aria-label': i18n.get('bacs.shopperEmail'),
'aria-required': 'true',
required: true,
readonly: status === CONFIRM_STATE || status === 'loading',
autocorrect: 'off',
onInput: handleEventFor('shopperEmail', 'input'),
onChange: handleEventFor('shopperEmail', 'blur')
})}
</Field>

{status === ENTER_STATE && (
<ConsentCheckbox
data={data}
errorMessage={!!errors.amountConsentCheckbox}
label={i18n.get('bacs.consent.amount')}
onChange={handleConsentCheckbox('amountConsentCheckbox')}
checked={!!data.amountConsentCheckbox}
/>
)}

{status === ENTER_STATE && (
<ConsentCheckbox
data={data}
errorMessage={!!errors.accountConsentCheckbox}
label={i18n.get('bacs.consent.account')}
onChange={handleConsentCheckbox('accountConsentCheckbox')}
checked={!!data.accountConsentCheckbox}
/>
)}

{props.showPayButton &&
props.payButton({
status,
label:
status === ENTER_STATE
? i18n.get('continue')
: `${i18n.get('bacs.confirm')} ${
!!props.amount?.value && !!props.amount?.currency ? i18n.amount(props.amount.value, props.amount.currency) : ''
}`,
icon: getImage({ loadingContext: props.loadingContext, imageFolder: 'components/' })('lock'),
onClick: handlePayButton
})}
</div>
);
}

BacsInput.defaultProps = {
data: {},
placeholders: {}
};

export default BacsInput;
6 changes: 6 additions & 0 deletions packages/lib/src/components/BacsDD/components/BacsResult.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@import '../../../style/index';

.adyen-checkout__voucher-result__introduction {
font-size: $font-size-medium;
max-width: 420px;
}