diff --git a/.changeset/neat-tigers-sniff.md b/.changeset/neat-tigers-sniff.md new file mode 100644 index 0000000000..8b6249a05c --- /dev/null +++ b/.changeset/neat-tigers-sniff.md @@ -0,0 +1,5 @@ +--- +'@adyen/adyen-web': minor +--- + +Feature: Allow to store French MealVoucher cards diff --git a/packages/lib/src/components/Giftcard/Giftcard.tsx b/packages/lib/src/components/Giftcard/Giftcard.tsx index 1b8133e442..5ef8eeda8e 100644 --- a/packages/lib/src/components/Giftcard/Giftcard.tsx +++ b/packages/lib/src/components/Giftcard/Giftcard.tsx @@ -29,12 +29,21 @@ export class GiftcardElement extends UIElement { 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; } @@ -43,6 +52,11 @@ export class GiftcardElement extends UIElement { } 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; } @@ -91,13 +105,12 @@ export class GiftcardElement extends UIElement { } 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; this.componentRef.setBalance({ balance, transactionLimit }); if (this.props.amount.value > balance.value || this.props.amount.value > transactionLimit.value) { diff --git a/packages/lib/src/components/Giftcard/components/GiftcardComponent.tsx b/packages/lib/src/components/Giftcard/components/GiftcardComponent.tsx index 1582107422..c0d389afe1 100644 --- a/packages/lib/src/components/Giftcard/components/GiftcardComponent.tsx +++ b/packages/lib/src/components/Giftcard/components/GiftcardComponent.tsx @@ -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; @@ -18,10 +20,16 @@ interface GiftcardComponentProps { amount: PaymentAmount; showPayButton?: boolean; payButton: (config) => any; + brand: string; pinRequired: boolean; expiryDateRequired?: boolean; fieldsLayoutComponent: FunctionComponent; + + enableStoreDetails: boolean; + storedPaymentMethodId: string; + expiryMonth?: number; + expiryYear?: number; } class Giftcard extends Component { @@ -45,15 +53,24 @@ class Giftcard extends Component { 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) { @@ -75,7 +92,7 @@ class Giftcard extends Component { 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; @@ -109,22 +126,49 @@ class Giftcard extends Component { 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 ( + + ); + } + + 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 && } + {this.props.showPayButton && this.props.payButton({ status: this.state.status, diff --git a/packages/lib/src/components/Giftcard/components/StoredGiftCardFields.tsx b/packages/lib/src/components/Giftcard/components/StoredGiftCardFields.tsx new file mode 100644 index 0000000000..5c3c46122f --- /dev/null +++ b/packages/lib/src/components/Giftcard/components/StoredGiftCardFields.tsx @@ -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) => { + 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 +
+
+ {expiryMonth && expiryYear && ( + + + + )} + {pinRequired && } +
+
+ ); +}; diff --git a/packages/lib/src/components/Giftcard/components/types.ts b/packages/lib/src/components/Giftcard/components/types.ts index 3fe7bedfc8..ba5ff14042 100644 --- a/packages/lib/src/components/Giftcard/components/types.ts +++ b/packages/lib/src/components/Giftcard/components/types.ts @@ -10,6 +10,7 @@ export type GiftcardFieldsProps = { focusedElement; setFocusOn; label?: string; + enableStoreDetails?: boolean; }; export type GiftcardFieldProps = { diff --git a/packages/lib/src/components/MealVoucherFR/MealVoucherFR.tsx b/packages/lib/src/components/MealVoucherFR/MealVoucherFR.tsx index 3b37f3094a..d9951c86a2 100644 --- a/packages/lib/src/components/MealVoucherFR/MealVoucherFR.tsx +++ b/packages/lib/src/components/MealVoucherFR/MealVoucherFR.tsx @@ -9,6 +9,7 @@ export class MealVoucherFRElement extends GiftcardElement { ...props, pinRequired: true, expiryDateRequired: true, + enableStoreDetails: true, fieldsLayoutComponent: MealVoucherFields }); } @@ -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() }; } } diff --git a/packages/lib/src/components/internal/StoreDetails/StoreDetails.tsx b/packages/lib/src/components/internal/StoreDetails/StoreDetails.tsx index 421b5dc5fc..38427c1b25 100644 --- a/packages/lib/src/components/internal/StoreDetails/StoreDetails.tsx +++ b/packages/lib/src/components/internal/StoreDetails/StoreDetails.tsx @@ -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); diff --git a/packages/lib/src/core/ProcessResponse/PaymentMethodsResponse/filters.ts b/packages/lib/src/core/ProcessResponse/PaymentMethodsResponse/filters.ts index c73245f676..bc1ba5b090 100644 --- a/packages/lib/src/core/ProcessResponse/PaymentMethodsResponse/filters.ts +++ b/packages/lib/src/core/ProcessResponse/PaymentMethodsResponse/filters.ts @@ -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); diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index b8ac2f7850..dfb40e083c 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -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'; diff --git a/packages/playground/src/config/paymentsConfig.js b/packages/playground/src/config/paymentsConfig.js index ac7d553974..70af5c53dd 100644 --- a/packages/playground/src/config/paymentsConfig.js +++ b/packages/playground/src/config/paymentsConfig.js @@ -56,6 +56,7 @@ const paymentsConfig = { taxCategory: 'None', amountExcludingTax: 10000 } - ] + ], + recurringProcessingModel: 'CardOnFile' }; export default paymentsConfig; diff --git a/packages/playground/src/services.js b/packages/playground/src/services.js index 86dfc2dd19..9c4ef4a359 100644 --- a/packages/playground/src/services.js +++ b/packages/playground/src/services.js @@ -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'; @@ -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;