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

Adds ANCV payment method component #2293

Merged
merged 16 commits into from
Oct 11, 2023
132 changes: 132 additions & 0 deletions packages/lib/src/components/ANCV/ANCV.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { h } from 'preact';
import UIElement from '../UIElement';
import ANCVInput from './components/ANCVInput';
import CoreProvider from '../../core/Context/CoreProvider';
import config from './components/ANCVAwait/config';
import Await from '../../components/internal/Await';
import SRPanelProvider from '../../core/Errors/SRPanelProvider';
import { PaymentResponse, UIElementProps } from '../types';
import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError';
import PayButton from '../internal/PayButton';

export interface ANCVProps extends UIElementProps {
paymentData?: any;
data: ANCVDataState;
onOrderRequest?: any;
onOrderCreated?: any;
}

export interface ANCVDataState {
beneficiaryId: string;
}

export class ANCVElement extends UIElement<ANCVProps> {
private static type = 'ancv';

/**
* Formats the component data output
*/
formatData() {
return {
paymentMethod: {
type: ANCVElement.type,
beneficiaryId: this.state.data?.beneficiaryId
}
};
}

private onOrderRequest = data => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic should be move elsewhere.

if (this.props.onOrderRequest)
return new Promise((resolve, reject) => {
this.props.onOrderRequest(resolve, reject, data);
});

if (this.props.session) {
return this.props.session.createOrder();
}
};

protected handleOrder = ({ order }: PaymentResponse) => {
this.updateParent({ order });
if (this.props.session && this.props.onOrderCreated) {
return this.props.onOrderCreated(order);
}
};

public createOrder = () => {
if (!this.isValid) {
this.showValidation();
return false;
}

this.setStatus('loading');

return this.onOrderRequest(this.data)
.then((order: { orderData: string; pspReference: string }) => {
this.setState({ order: { orderData: order.orderData, pspReference: order.pspReference } });
this.submit();
})
.catch(error => {
this.setStatus(error?.message || 'error');
if (this.props.onError) this.handleError(new AdyenCheckoutError('ERROR', error));
});
};

// Reimplement payButton similar to GiftCard to allow to set onClick
public payButton = props => {
return <PayButton {...props} />;
};

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

get displayName(): string {
return this.props.name;
}

render() {
if (this.props.paymentData) {
return (
<CoreProvider i18n={this.props.i18n} loadingContext={this.props.loadingContext} resources={this.resources}>
<SRPanelProvider srPanel={this.props.modules.srPanel}>
<Await
ref={ref => {
this.componentRef = ref;
}}
clientKey={this.props.clientKey}
paymentData={this.props.paymentData}
onError={this.props.onError}
onComplete={this.onComplete}
brandLogo={this.icon}
type={this.constructor['type']}
messageText={this.props.i18n.get('ancv.confirmPayment')}
awaitText={this.props.i18n.get('await.waitForConfirmation')}
showCountdownTimer={config.showCountdownTimer}
throttleTime={config.THROTTLE_TIME}
throttleInterval={config.THROTTLE_INTERVAL}
onActionHandled={this.props.onActionHandled}
/>
</SRPanelProvider>
</CoreProvider>
);
}

return (
<CoreProvider i18n={this.props.i18n} loadingContext={this.props.loadingContext} resources={this.resources}>
<ANCVInput
ref={ref => {
this.componentRef = ref;
}}
{...this.props}
onSubmit={this.createOrder}
onChange={this.setState}
payButton={this.payButton}
showPayButton={this.props.showPayButton}
/>
</CoreProvider>
);
}
}

export default ANCVElement;
10 changes: 10 additions & 0 deletions packages/lib/src/components/ANCV/components/ANCVAwait/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const COUNTDOWN_MINUTES = 15; // min
export const THROTTLE_TIME = 60000; // ms
export const THROTTLE_INTERVAL = 10000; // ms

export default {
COUNTDOWN_MINUTES,
THROTTLE_TIME,
THROTTLE_INTERVAL,
showCountdownTimer: false
};
64 changes: 64 additions & 0 deletions packages/lib/src/components/ANCV/components/ANCVInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import useCoreContext from '../../../core/Context/useCoreContext';
import LoadingWrapper from '../../internal/LoadingWrapper';
import InputText from '../../internal/FormFields/InputText';
import Field from '../../internal/FormFields/Field';
import useForm from '../../../utils/useForm';
import { UIElementProps } from '../../types';
import { ancvValidationRules } from '../validate';
import { ANCVDataState } from '../ANCV';

export interface ANCVInputProps extends UIElementProps {
ref?: any;
showPayButton: boolean;
onSubmit: () => void;
}

type ANCVInputDataState = ANCVDataState;

function ANCVInput({ showPayButton, payButton, onChange, onSubmit }: ANCVInputProps) {
const { i18n } = useCoreContext();

const { handleChangeFor, triggerValidation, data, valid, errors, isValid } = useForm<ANCVInputDataState>({
schema: ['beneficiaryId'],
rules: ancvValidationRules
});

useEffect(() => {
onChange({ data, errors, valid, isValid }, this);
}, [data, valid, errors, isValid]);

const [status, setStatus] = useState<string>('ready');

this.setStatus = setStatus;
this.showValidation = triggerValidation;

return (
<LoadingWrapper>
<div className="adyen-checkout__ancv">
<p className="adyen-checkout-form-instruction">{i18n.get('ancv.form.instruction')}</p>
<Field
errorMessage={!!errors.beneficiaryId && i18n.get(errors.beneficiaryId.errorMessage)}
label={i18n.get('ancv.input.label')}
isValid={valid.beneficiaryId}
name={'beneficiaryId'}
>
<InputText
value={data.beneficiaryId}
name={'beneficiaryId'}
spellcheck={true}
required={true}
onInput={handleChangeFor('beneficiaryId', 'input')}
onBlur={handleChangeFor('beneficiaryId', 'blur')}
/>
</Field>
{showPayButton && payButton({ status, label: i18n.get('confirmPurchase'), onClick: onSubmit })}
</div>
</LoadingWrapper>
);
}

ANCVInput.defaultProps = {};

export default ANCVInput;
1 change: 1 addition & 0 deletions packages/lib/src/components/ANCV/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ANCV';
12 changes: 12 additions & 0 deletions packages/lib/src/components/ANCV/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ValidatorRules } from '../../utils/Validator/types';
import { isEmailValid } from '../internal/PersonalDetails/validate';

export const isANCVNumber = text => /^\d{11}$/.test(text);

export const ancvValidationRules: ValidatorRules = {
beneficiaryId: {
validate: value => isEmailValid(value) || isANCVNumber(value),
errorMessage: 'ancv.beneficiaryId.invalid',
modes: ['blur']
}
};
2 changes: 2 additions & 0 deletions packages/lib/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import OnlineBankingSKElement from './OnlineBankingSK';
import PayByBank from './PayByBank';
import PromptPay from './PromptPay';
import Duitnow from './DuitNow';
import ANCV from './ANCV';
import Trustly from './Trustly';

/**
Expand Down Expand Up @@ -216,6 +217,7 @@ const componentsMap = {
upi: UPI, // also QR
upi_qr: UPI, // also QR
upi_collect: UPI, // also QR
ancv: ANCV,
/** Await */

/** Giftcard */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const isDateOfBirthValid = value => {
const age = new Date(ageDiff).getFullYear() - 1970;
return age >= 18;
};
const isEmailValid = value => {
export const isEmailValid = value => {
if (isEmpty(value)) return null;
return value.length >= 6 && value.length <= 320 && email.test(value);
};
Expand Down
6 changes: 5 additions & 1 deletion packages/lib/src/language/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -294,5 +294,9 @@
"form.instruction": "All fields are required unless marked otherwise.",
"trustly.descriptor": "Instant Bank Payment",
"trustly.description1": "Pay directly from any of your bank accounts, backed by bank-level security",
"trustly.description2": "No cards, no app download, no registration"
"trustly.description2": "No cards, no app download, no registration",
"ancv.input.label" : "Your ANCV identification",
"ancv.confirmPayment": "Use your ANCV application to confirm the payment.",
"ancv.form.instruction": "The Cheque-Vacances application is necessary to validate this payment.",
"ancv.beneficiaryId.invalid": "Enter a valid email address or ANCV ID"
}
24 changes: 24 additions & 0 deletions packages/lib/storybook/stories/components/ANCV.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { Container } from '../Container';
import { ANCVProps } from '../../../src/components/ANCV/ANCV';

type ANCVStory = StoryObj<PaymentMethodStoryProps<ANCVProps>>;

const meta: Meta<PaymentMethodStoryProps<ANCVProps>> = {
title: 'Components/ANCV'
};

export const ANCV: ANCVStory = {
render: (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'ancv'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
},
args: {
countryCode: 'NL',
amount: 2000,
useSessions: false
}
};
export default meta;
9 changes: 9 additions & 0 deletions packages/playground/src/pages/Dropin/manual.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ export async function initManual() {

if (result.action) {
component.handleAction(result.action);
} else if (result.order && result.order?.remainingAmount?.value > 0) {
// handle orders
const order = {
orderData: result.order.orderData,
pspReference: result.order.pspReference
};

const orderPaymentMethods = await getPaymentMethods({ order, amount, shopperLocale });
checkout.update({ paymentMethodsResponse: orderPaymentMethods, order, amount: result.order.remainingAmount });
} else {
handleFinalState(result.resultCode, component);
}
Expand Down
Loading