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

Feature: Allow to store French MealVoucher cards #2423

Draft
wants to merge 5 commits into
base: v5
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/neat-tigers-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': minor
---

Feature: Allow to store French MealVoucher cards
23 changes: 18 additions & 5 deletions packages/lib/src/components/Giftcard/Giftcard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,21 @@
type: this.constructor['type'],
brand: this.props.brand,
encryptedCardNumber: this.state.data?.encryptedCardNumber,
encryptedSecurityCode: this.state.data?.encryptedSecurityCode
encryptedSecurityCode: this.state.data?.encryptedSecurityCode,
...(this.props.storedPaymentMethodId && { storedPaymentMethodId: this.props.storedPaymentMethodId })
}
};
}

formatBalanceCheckData(): GiftCardElementData {
return this.formatData();
}

get isValid() {
if (this.props.storedPaymentMethodId) {
return true;
}

return !!this.state.isValid;
}

Expand All @@ -43,6 +52,11 @@
}

get displayName() {
if (this.props.storedPaymentMethodId && this.props.lastFour) {
// this applies for MealVoucher since it has the logic for lastFour
return `•••• ${this.props.lastFour}`;
}

return this.props.brandsConfiguration[this.props.brand]?.name || this.props.name;
}

Expand Down Expand Up @@ -91,13 +105,12 @@
}

this.setStatus('loading');

this.handleBalanceCheck(this.formatData())
.then(({ balance, transactionLimit = {} as PaymentAmount }) => {
this.handleBalanceCheck(this.formatBalanceCheckData())
.then(({ balance, transactionLimit = {} as PaymentAmount }: { balance: PaymentAmount; transactionLimit: PaymentAmount }) => {
if (!balance) throw new Error('card-error'); // card doesn't exist
if (balance?.currency !== this.props.amount?.currency) throw new Error('currency-error');
if (balance?.value <= 0) throw new Error('no-balance');

debugger;

Check warning on line 113 in packages/lib/src/components/Giftcard/Giftcard.tsx

View workflow job for this annotation

GitHub Actions / build-then-e2e (18.15)

Unexpected 'debugger' statement

Check warning on line 113 in packages/lib/src/components/Giftcard/Giftcard.tsx

View workflow job for this annotation

GitHub Actions / build-then-e2e (18.15)

Unexpected 'debugger' statement

Check warning on line 113 in packages/lib/src/components/Giftcard/Giftcard.tsx

View workflow job for this annotation

GitHub Actions / build-then-e2e (18.15)

Unexpected 'debugger' statement

Check warning on line 113 in packages/lib/src/components/Giftcard/Giftcard.tsx

View workflow job for this annotation

GitHub Actions / build (18.15)

Unexpected 'debugger' statement

Check warning on line 113 in packages/lib/src/components/Giftcard/Giftcard.tsx

View workflow job for this annotation

GitHub Actions / build (18.15)

Unexpected 'debugger' statement

Check warning on line 113 in packages/lib/src/components/Giftcard/Giftcard.tsx

View workflow job for this annotation

GitHub Actions / build (18.15)

Unexpected 'debugger' statement
this.componentRef.setBalance({ balance, transactionLimit });

if (this.props.amount.value > balance.value || this.props.amount.value > transactionLimit.value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { PaymentAmount } from '../../../types';
import { GIFT_CARD } from '../../internal/SecuredFields/lib/configuration/constants';
import { GiftCardFields } from './GiftcardFields';
import { GiftcardFieldsProps } from './types';
import StoreDetails from '../../internal/StoreDetails';
import { StoredGiftCardFields } from './StoredGiftCardFields';

interface GiftcardComponentProps {
onChange: (state) => void;
Expand All @@ -18,10 +20,16 @@ interface GiftcardComponentProps {
amount: PaymentAmount;
showPayButton?: boolean;
payButton: (config) => any;
brand: string;

pinRequired: boolean;
expiryDateRequired?: boolean;
fieldsLayoutComponent: FunctionComponent<GiftcardFieldsProps>;

enableStoreDetails: boolean;
storedPaymentMethodId: string;
expiryMonth?: number;
expiryYear?: number;
}

class Giftcard extends Component<GiftcardComponentProps> {
Expand All @@ -45,15 +53,24 @@ class Giftcard extends Component<GiftcardComponentProps> {

public sfp;

public onChange = sfpState => {
public handleSecureFieldsChange = sfpState => {
this.props.onChange({
data: sfpState.data,
isValid: sfpState.isSfpValid
});
};

public handleOnStoreDetails = storedDetails => {
this.props.onChange({
storePaymentMethod: storedDetails
});
};

public showValidation = () => {
this.sfp.showValidation();
// in case it's a stored gift card (stored mealvoucher) there will be no SFP
if (this.sfp) {
this.sfp.showValidation();
}
};

setStatus(status) {
Expand All @@ -75,7 +92,7 @@ class Giftcard extends Component<GiftcardComponentProps> {
this.setState({ balance, transactionLimit });
};

render(props, { focusedElement, balance, transactionLimit }) {
render(props: GiftcardComponentProps, { focusedElement, balance, transactionLimit }) {
const { i18n } = useCoreContext();

const transactionAmount = transactionLimit?.value < balance?.value ? transactionLimit : balance;
Expand Down Expand Up @@ -109,22 +126,49 @@ class Giftcard extends Component<GiftcardComponentProps> {
ref={ref => {
this.sfp = ref;
}}
onChange={this.onChange}
onChange={this.handleSecureFieldsChange}
onFocus={this.handleFocus}
type={GIFT_CARD}
render={({ setRootNode, setFocusOn }, sfpState) =>
this.props.fieldsLayoutComponent({
render={({ setRootNode, setFocusOn }, sfpState) => {
if (props.storedPaymentMethodId) {
// return this.props.payButton({
// status: this.state.status,
// onClick: this.props.onBalanceCheck,
// label: i18n.get('applyGiftcard'),
// classNameModifiers: ['standalone']
// });
return (
<StoredGiftCardFields
i18n={i18n}
pinRequired={this.props.pinRequired}
sfpState={sfpState}
focusedElement={focusedElement}
setFocusOn={setFocusOn}
setRootNode={setRootNode}
getCardErrorMessage={getCardErrorMessage}
expiryMonth={props.expiryMonth}
expiryYear={props.expiryYear}
{...props}
/>
);
}

return this.props.fieldsLayoutComponent({
i18n: i18n,
pinRequired: this.props.pinRequired,
focusedElement: focusedElement,
getCardErrorMessage: getCardErrorMessage,
setRootNode: setRootNode,
setFocusOn: setFocusOn,
sfpState: sfpState
})
}
sfpState: sfpState,
// TODO maybe remove this?
...props
});
}}
/>

{props.enableStoreDetails && <StoreDetails onChange={this.handleOnStoreDetails} />}

{this.props.showPayButton &&
this.props.payButton({
status: this.state.status,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { h } from 'preact';
import { GiftcardPinField } from './GiftcardPinField';
import { GiftcardFieldsProps } from './types';
import useCoreContext from '../../../core/Context/useCoreContext';
import Field from '../../internal/FormFields/Field';
import InputText from '../../internal/FormFields/InputText';

interface StoredGiftCardFieldsProps extends GiftcardFieldsProps {
expiryMonth: number;
expiryYear: number;
}

export const StoredGiftCardFields = (props: Readonly<StoredGiftCardFieldsProps>) => {
const { setRootNode, pinRequired, expiryMonth, expiryYear } = props;
const { i18n } = useCoreContext();
// const storedCardDescription = i18n.get('creditCard.storedCard.description.ariaLabel').replace('%@', lastFour);
// const storedCardDescriptionSuffix =
// expiryMonth && expiryYear ? ` ${i18n.get('creditCard.expiryDateField.title')} ${expiryMonth}/${expiryYear}` : '';
// const ariaLabel = `${storedCardDescription}${storedCardDescriptionSuffix}`;

// const getError = (errors, fieldType) => {
// const errorMessage = errors[fieldType] ? i18n.get(errors[fieldType]) : null;
// return errorMessage;
// };

return (
// TODO missing aria-label
<div className="adyen-checkout__card__form adyen-checkout__card__form--oneClick">
<div ref={setRootNode} className="adyen-checkout__card__exp-cvc adyen-checkout__field-wrapper">
{expiryMonth && expiryYear && (
<Field
label={i18n.get('creditCard.expiryDateField.title')}
className="adyen-checkout__field--50"
classNameModifiers={['storedCard']}
name={'expiryDateField'}
disabled
>
<InputText
name={'expiryDateField'}
className={'adyen-checkout__input adyen-checkout__input--disabled adyen-checkout__card__exp-date__input--oneclick'}
value={`${expiryMonth} / ${expiryYear}`}
readonly={true}
disabled={true}
dir={'ltr'}
/>
</Field>
)}
{pinRequired && <GiftcardPinField {...props} classNameModifiers={['50']} />}
</div>
</div>
);
};
1 change: 1 addition & 0 deletions packages/lib/src/components/Giftcard/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type GiftcardFieldsProps = {
focusedElement;
setFocusOn;
label?: string;
enableStoreDetails?: boolean;
};

export type GiftcardFieldProps = {
Expand Down
29 changes: 21 additions & 8 deletions packages/lib/src/components/MealVoucherFR/MealVoucherFR.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class MealVoucherFRElement extends GiftcardElement {
...props,
pinRequired: true,
expiryDateRequired: true,
enableStoreDetails: true,
fieldsLayoutComponent: MealVoucherFields
});
}
Expand All @@ -20,19 +21,31 @@ export class MealVoucherFRElement extends GiftcardElement {
};
}

private formatPaymentMethod() {
return {
type: this.constructor['type'],
brand: this.props.brand,
encryptedCardNumber: this.state.data?.encryptedCardNumber,
encryptedSecurityCode: this.state.data?.encryptedSecurityCode,
encryptedExpiryMonth: this.state.data?.encryptedExpiryMonth,
encryptedExpiryYear: this.state.data?.encryptedExpiryYear,
...(this.props.storedPaymentMethodId && { storedPaymentMethodId: this.props.storedPaymentMethodId })
};
}

/**
* Formats the component data output
*/
formatData() {
return {
paymentMethod: {
type: this.constructor['type'],
brand: this.props.brand,
encryptedCardNumber: this.state.data?.encryptedCardNumber,
encryptedSecurityCode: this.state.data?.encryptedSecurityCode,
encryptedExpiryMonth: this.state.data?.encryptedExpiryMonth,
encryptedExpiryYear: this.state.data?.encryptedExpiryYear
}
paymentMethod: this.formatPaymentMethod(),
...(this.state.storePaymentMethod && { storePaymentMethod: this.state.storePaymentMethod })
};
}

formatBalanceCheckData() {
return {
paymentMethod: this.formatPaymentMethod()
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import { h } from 'preact';
import useCoreContext from '../../../core/Context/useCoreContext';
import Checkbox from '../FormFields/Checkbox';

interface StoreDetailsProps {
storeDetails?: boolean;
onChange: any;
}

/**
* "Store details" generic checkbox
*/
function StoreDetails({ storeDetails = false, ...props }) {
function StoreDetails({ storeDetails = false, ...props }: StoreDetailsProps) {
const { i18n } = useCoreContext();
const [value, setValue] = useState(storeDetails);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ export function filterEcomStoredPaymentMethods(pm) {
return !!pm && !!pm.supportedShopperInteractions && pm.supportedShopperInteractions.includes('Ecommerce');
}

const supportedStoredPaymentMethods = ['scheme', 'blik', 'twint', 'ach', 'cashapp'];
const supportedStoredPaymentMethods = [
'scheme',
'blik',
'twint',
'ach',
'cashapp',
'mealVoucher_FR_groupeup',
'mealVoucher_FR_sodexo',
'mealVoucher_FR_natixis'
];

export function filterSupportedStoredPaymentMethods(pm) {
return !!pm && !!pm.type && supportedStoredPaymentMethods.includes(pm.type);
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
if (process.env.NODE_ENV === 'development') {
// Must use require here as import statements are only allowed
// to exist at the top of a file.
// require('preact/debug');
require('preact/debug');
}

import { CoreOptions } from './core/types';
Expand Down
3 changes: 2 additions & 1 deletion packages/playground/src/config/paymentsConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const paymentsConfig = {
taxCategory: 'None',
amountExcludingTax: 10000
}
]
],
recurringProcessingModel: 'CardOnFile'
};
export default paymentsConfig;
9 changes: 5 additions & 4 deletions packages/playground/src/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export const getPaymentMethods = configuration =>
export const makePayment = (data, config = {}) => {
// NOTE: Merging data object. DO NOT do this in production.
const paymentRequest = { ...paymentsConfig, ...config, ...data };
if (paymentRequest.order) {
delete paymentRequest.amount;
}
// if (paymentRequest.order) {
// delete paymentRequest.amount;
// }
return httpPost('payments', paymentRequest)
.then(response => {
if (response.error) throw 'Payment initiation failed';
Expand Down Expand Up @@ -49,7 +49,8 @@ export const getOriginKey = (originKeyOrigin = document.location.origin) =>
httpPost('originKeys', { originDomains: [originKeyOrigin] }).then(response => response.originKeys[originKeyOrigin]);

export const checkBalance = data => {
return httpPost('paymentMethods/balance', data)
const payload = { amount: paymentsConfig.amount, ...data };
return httpPost('paymentMethods/balance', payload)
.then(response => {
if (response.error) throw 'Balance call failed';
return response;
Expand Down
Loading