From 94afef0d7fa0619ac51c929db6ec51b00f3baa2c Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Mon, 13 Nov 2023 10:56:33 +0100 Subject: [PATCH 01/55] draft: async submit --- packages/lib/src/components/Dropin/Dropin.tsx | 10 +- .../src/components/GooglePay/GooglePay.tsx | 97 +++++++--- .../components/GooglePay/GooglePayService.ts | 6 +- .../src/components/GooglePay/defaultProps.ts | 4 +- .../lib/src/components/GooglePay/types.ts | 1 - .../lib/src/components/GooglePay/utils.ts | 30 +-- packages/lib/src/components/UIElement.tsx | 175 +++++++++++++----- packages/lib/src/components/types.ts | 2 +- packages/lib/src/core/types.ts | 4 +- .../playground/src/pages/Dropin/manual.js | 80 +++++++- 10 files changed, 306 insertions(+), 103 deletions(-) diff --git a/packages/lib/src/components/Dropin/Dropin.tsx b/packages/lib/src/components/Dropin/Dropin.tsx index fa5745a8ad..8b2a8b6525 100644 --- a/packages/lib/src/components/Dropin/Dropin.tsx +++ b/packages/lib/src/components/Dropin/Dropin.tsx @@ -84,11 +84,19 @@ class DropinElement extends UIElement<DropinElementProps> { /** * Calls the onSubmit event with the state of the activePaymentMethod */ - public submit(): void { + public async submit(): Promise<void> { if (!this.activePaymentMethod) { throw new Error('No active payment method.'); } + if (!this.activePaymentMethod.isValid) { + this.activePaymentMethod.showValidation(); + } + + if (this.activePaymentMethod.isInstantPayment) { + this.closeActivePaymentMethod(); + } + this.activePaymentMethod.submit(); } diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 1f4d2e1f58..df644f3c16 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -1,10 +1,10 @@ import { h } from 'preact'; -import UIElement from '../UIElement'; +import UIElement, { SubmitReject } from '../UIElement'; import GooglePayService from './GooglePayService'; import GooglePayButton from './components/GooglePayButton'; import defaultProps from './defaultProps'; import { GooglePayProps } from './types'; -import { mapBrands, getGooglePayLocale } from './utils'; +import { getGooglePayLocale } from './utils'; import collectBrowserInfo from '../../utils/browserInfo'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; @@ -14,23 +14,33 @@ class GooglePay extends UIElement<GooglePayProps> { public static txVariants = [TxVariants.googlepay, TxVariants.paywithgoogle]; public static defaultProps = defaultProps; - protected googlePay = new GooglePayService(this.props); + protected readonly googlePay; - /** - * Formats the component data input - * For legacy support - maps configuration.merchantIdentifier to configuration.merchantId - */ - formatProps(props): GooglePayProps { - const allowedCardNetworks = props.brands?.length ? mapBrands(props.brands) : props.allowedCardNetworks; + constructor(props) { + super(props); + + this.googlePay = new GooglePayService({ + ...this.props, + paymentDataCallbacks: { + ...this.props.paymentDataCallbacks, + onPaymentAuthorized: this.onPaymentAuthorized + } + }); + } + + formatProps(props: GooglePayProps): GooglePayProps { + // const allowedCardNetworks = props.brands?.length ? mapBrands(props.brands) : props.allowedCardNetworks; BRANDS not documented const buttonSizeMode = props.buttonSizeMode ?? (props.isDropin ? 'fill' : 'static'); const buttonLocale = getGooglePayLocale(props.buttonLocale ?? props.i18n?.locale); + + const callbackIntents: google.payments.api.CallbackIntent[] = [...props.callbackIntents, 'PAYMENT_AUTHORIZATION']; + return { ...props, - showButton: props.showPayButton === true, configuration: props.configuration, - allowedCardNetworks, buttonSizeMode, - buttonLocale + buttonLocale, + callbackIntents }; } @@ -41,25 +51,18 @@ class GooglePay extends UIElement<GooglePayProps> { return { paymentMethod: { type: this.type, - ...this.state + googlePayCardNetwork: this.state.googlePayCardNetwork, + googlePayToken: '{}' }, browserInfo: this.browserInfo }; } public submit = () => { - const { onAuthorized = () => {} } = this.props; - return new Promise((resolve, reject) => this.props.onClick(resolve, reject)) .then(() => this.googlePay.initiatePayment(this.props)) - .then(paymentData => { - this.setState({ - googlePayToken: paymentData.paymentMethodData.tokenizationData.token, - googlePayCardNetwork: paymentData.paymentMethodData.info.cardNetwork - }); - super.submit(); - - return onAuthorized(paymentData); + .then(() => { + console.log('HERE'); }) .catch((error: google.payments.api.PaymentsError) => { if (error.statusCode === 'CANCELED') { @@ -70,6 +73,54 @@ class GooglePay extends UIElement<GooglePayProps> { }); }; + /** + * Method called when the payment is authorized in the payment sheet + * + * @see https://developers.google.com/pay/api/web/reference/client#onPaymentAuthorized + **/ + private onPaymentAuthorized = async (paymentData: google.payments.api.PaymentData): Promise<google.payments.api.PaymentAuthorizationResult> => { + this.setState({ + authorizedData: paymentData, + googlePayToken: paymentData.paymentMethodData.tokenizationData.token, + googlePayCardNetwork: paymentData.paymentMethodData.info.cardNetwork + }); + + return new Promise<google.payments.api.PaymentAuthorizationResult>(resolve => { + super + .submit() + .then(result => { + console.log('Resolving'); + + resolve({ transactionState: 'SUCCESS' }); + this.props.onPaymentCompleted(result, this.elementRef); + }) + .catch(googlePayError => { + console.log('Caught error'); + + resolve({ + transactionState: 'ERROR', + error: { + intent: 'PAYMENT_AUTHORIZATION', + message: googlePayError?.message || 'Something went wrong', + reason: googlePayError?.reason || 'OTHER_ERROR' + } + }); + }); + }); + }; + + protected override throwPaymentMethodErrorIfNeeded = (error?: SubmitReject): never => { + this.setElementStatus('ready'); + + const googleError: google.payments.api.PaymentDataError = { + intent: 'PAYMENT_AUTHORIZATION', + message: error?.googlePayError?.message || 'Something went wrong', + reason: error?.googlePayError?.reason || 'OTHER_ERROR' + }; + + throw googleError; + }; + /** * Validation */ diff --git a/packages/lib/src/components/GooglePay/GooglePayService.ts b/packages/lib/src/components/GooglePay/GooglePayService.ts index 61ac959ead..c6b8c6db77 100644 --- a/packages/lib/src/components/GooglePay/GooglePayService.ts +++ b/packages/lib/src/components/GooglePay/GooglePayService.ts @@ -3,10 +3,14 @@ import { resolveEnvironment } from './utils'; import Script from '../../utils/Script'; import config from './config'; +interface GooglePayServiceProps { + [key: string]: any; +} + class GooglePayService { public readonly paymentsClient: Promise<google.payments.api.PaymentsClient>; - constructor(props) { + constructor(props: GooglePayServiceProps) { const environment = resolveEnvironment(props.environment); if (environment === 'TEST' && process.env.NODE_ENV === 'development') { console.warn('Google Pay initiated in TEST mode. Request non-chargeable payment methods suitable for testing.'); diff --git a/packages/lib/src/components/GooglePay/defaultProps.ts b/packages/lib/src/components/GooglePay/defaultProps.ts index 5662ad1e41..cf4e646aaa 100644 --- a/packages/lib/src/components/GooglePay/defaultProps.ts +++ b/packages/lib/src/components/GooglePay/defaultProps.ts @@ -48,5 +48,7 @@ export default { shippingAddressParameters: undefined, // https://developers.google.com/pay/api/web/reference/object#ShippingAddressParameters shippingOptionRequired: false, shippingOptionParameters: undefined, - paymentMethods: [] + paymentMethods: [], + + callbackIntents: [] }; diff --git a/packages/lib/src/components/GooglePay/types.ts b/packages/lib/src/components/GooglePay/types.ts index ec0ad1b216..5e0bff0570 100644 --- a/packages/lib/src/components/GooglePay/types.ts +++ b/packages/lib/src/components/GooglePay/types.ts @@ -35,7 +35,6 @@ export interface GooglePayPropsConfiguration { export interface GooglePayProps extends UIElementProps { type?: 'googlepay' | 'paywithgoogle'; - environment?: google.payments.api.Environment | string; configuration?: GooglePayPropsConfiguration; /** diff --git a/packages/lib/src/components/GooglePay/utils.ts b/packages/lib/src/components/GooglePay/utils.ts index aec6d7b700..737a75e748 100644 --- a/packages/lib/src/components/GooglePay/utils.ts +++ b/packages/lib/src/components/GooglePay/utils.ts @@ -16,21 +16,21 @@ export function resolveEnvironment(env = 'TEST'): google.payments.api.Environmen } } -export function mapBrands(brands) { - const brandMapping = { - mc: 'MASTERCARD', - amex: 'AMEX', - visa: 'VISA', - interac: 'INTERAC', - discover: 'DISCOVER' - }; - return brands.reduce((accumulator, item) => { - if (!!brandMapping[item] && !accumulator.includes(brandMapping[item])) { - accumulator.push(brandMapping[item]); - } - return accumulator; - }, []); -} +// export function mapBrands(brands) { +// const brandMapping = { +// mc: 'MASTERCARD', +// amex: 'AMEX', +// visa: 'VISA', +// interac: 'INTERAC', +// discover: 'DISCOVER' +// }; +// return brands.reduce((accumulator, item) => { +// if (!!brandMapping[item] && !accumulator.includes(brandMapping[item])) { +// accumulator.push(brandMapping[item]); +// } +// return accumulator; +// }, []); +// } const supportedLocales = [ 'en', diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 8a83a23822..75991529e0 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -1,6 +1,6 @@ import { h } from 'preact'; import BaseElement from './BaseElement'; -import { PaymentAction } from '../types'; +import { CheckoutSessionPaymentResponse, PaymentAction } from '../types'; import { ComponentMethodsRef, PaymentResponse } from './types'; import PayButton from './internal/PayButton'; import { IUIElement, PayButtonFunctionProps, RawPaymentResponse, UIElementProps } from './types'; @@ -8,12 +8,22 @@ import { getSanitizedResponse, resolveFinalResult } from './utils'; import AdyenCheckoutError from '../core/Errors/AdyenCheckoutError'; import { UIElementStatus } from './types'; import { hasOwnProperty } from '../utils/hasOwnProperty'; -import DropinElement from './Dropin'; import { CoreOptions, ICore } from '../core/types'; import { Resources } from '../core/Context/Resources'; import { NewableComponent } from '../core/core.registry'; import './UIElement.scss'; +export type SubmitReject = { + googlePayError?: { + message?: string; + reason?: google.payments.api.ErrorReason; + }; + applePayError?: { + // TOOD + [key: string]: any; + }; +}; + export abstract class UIElement<P extends UIElementProps = UIElementProps> extends BaseElement<P> implements IUIElement { protected componentRef: any; @@ -40,10 +50,10 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten this.setState = this.setState.bind(this); this.onValid = this.onValid.bind(this); this.onComplete = this.onComplete.bind(this); - this.onSubmit = this.onSubmit.bind(this); + this.makePaymentsCall = this.makePaymentsCall.bind(this); this.handleAction = this.handleAction.bind(this); this.handleOrder = this.handleOrder.bind(this); - this.handleResponse = this.handleResponse.bind(this); + this.handleSessionsResponse = this.handleSessionsResponse.bind(this); this.setElementStatus = this.setElementStatus.bind(this); this.elementRef = (props && props.elementRef) || this; @@ -82,6 +92,9 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten public setState(newState: object): void { this.state = { ...this.state, ...newState }; + + console.log('new state', this.state); + this.onChange(); } @@ -94,41 +107,106 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten return state; } - private onSubmit(): void { - //TODO: refactor this, instant payment methods are part of Dropin logic not UIElement - if (this.props.isInstantPayment) { - const dropinElementRef = this.elementRef as unknown as DropinElement; - dropinElementRef.closeActivePaymentMethod(); - } - + /** + * Triggers the payment flow + */ + private async makePaymentsCall(): Promise<void> { if (this.props.setStatusAutomatically) { this.setElementStatus('loading'); } if (this.props.onSubmit) { - // Classic flow - this.props.onSubmit({ data: this.data, isValid: this.isValid }, this.elementRef); - } else if (this.core.session) { - // Session flow - // wrap beforeSubmit callback in a promise - const beforeSubmitEvent = this.props.beforeSubmit - ? new Promise((resolve, reject) => - this.props.beforeSubmit(this.data, this.elementRef, { - resolve, - reject - }) - ) - : Promise.resolve(this.data); - - beforeSubmitEvent - .then(data => this.submitPayment(data)) - .catch(() => { - // set state as ready to submit if the merchant cancels the action - this.elementRef.setStatus('ready'); - }); - } else { - this.handleError(new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Could not submit the payment')); + return this.submitUsingAdvancedFlow(); + } + + if (this.core.session) { + return this.submitUsingSessionsFlow(); } + + this.handleError(new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Could not submit the payment')); + } + + private async submitUsingAdvancedFlow() { + return ( + new Promise((resolve, reject) => { + this.props.onSubmit( + { + data: this.data, + isValid: this.isValid, + ...(this.state.authorizedData && { authorizedData: this.state.authorizedData }) + }, + this.elementRef, + { resolve, reject } + ); + }) + .then((data: any) => { + if (data.action) { + this.elementRef.handleAction(data.action, ...data.actionProps); + return; + } + if (data.order) { + const { order, paymentMethodsResponse } = data; + // @ts-ignore Just testing + this.core.update({ paymentMethodsResponse, order, amount: data.order.remainingAmount }); + return; + } + + this.handleFinalResult(data); + }) + // action.reject got called OR something fail above. TODO: add proper checks + .catch(error => { + this.throwPaymentMethodErrorIfNeeded(error); + }) + ); + } + + private async submitUsingSessionsFlow() { + const beforeSubmitEvent = this.props.beforeSubmit + ? new Promise((resolve, reject) => + this.props.beforeSubmit(this.data, this.elementRef, { + resolve, + reject + }) + ) + : Promise.resolve(this.data); + + let data; + + try { + data = await beforeSubmitEvent; + } catch { + // set state as ready to submit if the merchant cancels the action + this.elementRef.setStatus('ready'); + return; + } + + return this.makeSessionPaymentsCall(data); + } + + /** + * Method used to break the /payments flow and feed the error data back to the component in case the + * payment fails. + * + * Example: GooglePay / ApplePay accepts data from merchant in order to display custom errors + * + * @param error - Error object that can be passed back by the merchant + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected throwPaymentMethodErrorIfNeeded(error?: SubmitReject): void | never { + return; + } + + private async makeSessionPaymentsCall(data): Promise<void> { + let paymentsResponse: CheckoutSessionPaymentResponse = null; + + try { + paymentsResponse = await this.core.session.submitPayment(data); + } catch (error) { + this.handleError(error); + this.throwPaymentMethodErrorIfNeeded(); + } + + this.handleSessionsResponse(paymentsResponse); } private onValid() { @@ -144,13 +222,13 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten /** * Submit payment method data. If the form is not valid, it will trigger validation. */ - public submit(): void { + public async submit(): Promise<void> { if (!this.isValid) { this.showValidation(); return; } - this.onSubmit(); + return this.makePaymentsCall(); } public showValidation(): this { @@ -170,15 +248,8 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten return this; } - private submitPayment(data): Promise<void> { - return this.core.session - .submitPayment(data) - .then(this.handleResponse) - .catch(error => this.handleError(error)); - } - private submitAdditionalDetails(data): Promise<void> { - return this.core.session.submitDetails(data).then(this.handleResponse).catch(this.handleError); + return this.core.session.submitDetails(data).then(this.handleSessionsResponse).catch(this.handleError); } protected handleError = (error: AdyenCheckoutError): void => { @@ -236,13 +307,16 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten }; protected handleFinalResult = (result: PaymentResponse) => { - if (this.props.setStatusAutomatically) { - const [status, statusProps] = resolveFinalResult(result); - if (status) this.setElementStatus(status, statusProps); + const [status, statusProps] = resolveFinalResult(result); + + if (this.props.setStatusAutomatically && status) { + this.setElementStatus(status, statusProps); } - if (this.props.onPaymentCompleted) this.props.onPaymentCompleted(result, this.elementRef); - return result; + if (this.props.onPaymentCompleted) { + this.props.onPaymentCompleted(result, this.elementRef); + } + // return result; }; /** @@ -250,7 +324,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten * The component will handle automatically actions, orders, and final results. * @param rawResponse - */ - protected handleResponse(rawResponse: RawPaymentResponse): void { + protected handleSessionsResponse(rawResponse: RawPaymentResponse): void { const response = getSanitizedResponse(rawResponse); if (response.action) { @@ -260,7 +334,8 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten // we do this way so the logic on handlingOrder is associated with payment method this.handleOrder(response); } else { - this.elementRef.handleFinalResult(response); + this.handleFinalResult(response); + // this.elementRef.handleFinalResult(response); } } diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 065e1bbedc..3dce7c2a15 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -373,7 +373,7 @@ export interface UIElementProps extends BaseElementProps { onChange?: (state: any, element: UIElement) => void; onValid?: (state: any, element: UIElement) => void; beforeSubmit?: (state: any, element: UIElement, actions: any) => Promise<void>; - onSubmit?: (state: any, element: UIElement) => void; + onSubmit?: (state: any, element: UIElement, actions: any) => void; onComplete?: (state, element: UIElement) => void; onActionHandled?: (rtnObj: ActionHandledReturnObject) => void; onAdditionalDetails?: (state: any, element: UIElement) => void; diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index f8e402f3ae..d576585fbc 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -30,12 +30,14 @@ export interface ICore { session?: Session; } +export type AdyenEnvironment = 'test' | 'live' | 'live-us' | 'live-au' | 'live-apse' | 'live-in' | string; + export interface CoreOptions { session?: any; /** * Use test. When you're ready to accept live payments, change the value to one of our {@link https://docs.adyen.com/checkout/drop-in-web#testing-your-integration | live environments}. */ - environment?: 'test' | 'live' | 'live-us' | 'live-au' | 'live-apse' | 'live-in' | string; + environment?: AdyenEnvironment; /** * Used internally by Pay By Link in order to set its own URL's instead of using the ones mapped in our codebase. diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index 281691e6ae..e33136c979 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -19,27 +19,89 @@ export async function initManual() { // translationFile: nl_NL, environment: process.env.__CLIENT_ENV__, - onSubmit: async (state, component) => { + + onSubmit: async (state, component, actions) => { + const { authorizedData } = state; + console.log('authorizedData', authorizedData); + const result = await makePayment(state.data); - // handle actions if (result.action) { - // demo only - store paymentData & order - if (result.action.paymentData) localStorage.setItem('storedPaymentData', result.action.paymentData); - component.handleAction(result.action, { challengeWindowSize: '01' }); - } else if (result.order && result.order?.remainingAmount?.value > 0) { - // handle orders + actions.resolve({ + action: result.action, + actionProps: { + challengeWindowSize: '01' + } + }); + return; + } + + if (result.order && result.order?.remainingAmount?.value > 0) { 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 }); + + actions.resolve({ + order, + paymentMethodsResponse: orderPaymentMethods + }); + + return; + } + + if (result.resultCode === 'Authorised' || result.resultCode === 'Received') { + actions.resolve(result); // DO I NEED FULL RESULT? } else { - handleFinalState(result.resultCode, component); + actions.reject(result) } + + + // + // // Trigger Error for GooglePay + // // actions.reject({ + // // googlePayError: { + // // message: 'Not sufficient funds', + // // reason: 'OTHER_ERROR,' + // // } + // // }); + // + // actions.resolve({ resultCode: result.resultCode }); + }, + + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); }, + // + // + // // payments call + // if (result === AUTHORIZED) + // return action.resolve({ orderTrackng: { /// details }})} + // } + // + // + // action.reject(new ApplePayError(''))); + + // // handle actions + // if (result.action) { + // // demo only - store paymentData & order + // if (result.action.paymentData) localStorage.setItem('storedPaymentData', result.action.paymentData); + // component.handleAction(result.action, { challengeWindowSize: '01' }); + // } 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); + // } + // }, // srConfig: { showPanel: true }, // onChange: state => { // console.log('onChange', state); From 0011106c254bd25f3131303343d84dc5cd123777 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Wed, 29 Nov 2023 11:45:29 +0100 Subject: [PATCH 02/55] temp: disabling errors --- .../lib/src/components/ApplePay/ApplePay.tsx | 5 +- .../src/components/CashAppPay/CashAppPay.tsx | 2 +- packages/lib/src/components/PayPal/Paypal.tsx | 2 +- yarn.lock | 66 ++----------------- 4 files changed, 11 insertions(+), 64 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index eced99ddf4..f7e4b18073 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -53,9 +53,10 @@ class ApplePayElement extends UIElement<ApplePayElementProps> { }; } - submit() { + // @ts-ignore FIX THIS + public submit = async () => { return this.startSession(this.props.onAuthorized); - } + }; private startSession(onPaymentAuthorized: OnAuthorizedCallback) { const { version, onValidateMerchant, onPaymentMethodSelected, onShippingMethodSelected, onShippingContactSelected } = this.props; diff --git a/packages/lib/src/components/CashAppPay/CashAppPay.tsx b/packages/lib/src/components/CashAppPay/CashAppPay.tsx index 9784c7fcbe..526eb1f03b 100644 --- a/packages/lib/src/components/CashAppPay/CashAppPay.tsx +++ b/packages/lib/src/components/CashAppPay/CashAppPay.tsx @@ -93,7 +93,7 @@ export class CashAppPay extends UIElement<CashAppPayElementProps> { return this.props.storedPaymentMethodId ? 'Cash App Pay' : ''; } - public submit = () => { + public submit = async () => { const { onClick, storedPaymentMethodId } = this.props; if (storedPaymentMethodId) { diff --git a/packages/lib/src/components/PayPal/Paypal.tsx b/packages/lib/src/components/PayPal/Paypal.tsx index f1e48fafc5..02a5abba0d 100644 --- a/packages/lib/src/components/PayPal/Paypal.tsx +++ b/packages/lib/src/components/PayPal/Paypal.tsx @@ -42,7 +42,7 @@ class PaypalElement extends UIElement<PayPalElementProps> { }; } - public submit = () => { + public submit = async () => { this.handleError(new AdyenCheckoutError('IMPLEMENTATION_ERROR', ERRORS.SUBMIT_NOT_SUPPORTED)); }; diff --git a/yarn.lock b/yarn.lock index a4a5b8e9bb..cf6da97917 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2506,14 +2506,7 @@ "@types/node" "*" jest-mock "^29.7.0" -"@jest/expect-utils@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.6.3.tgz#5ef1a9689fdaa348da837c8be8d1219f56940ea3" - integrity sha512-nvOEW4YoqRKD9HBJ9OJ6przvIvP9qilp5nAn1462P5ZlL/MM9SgPEZFyjTGPfs7QkocdUsJa6KjHhyRn4ueItA== - dependencies: - jest-get-type "^29.6.3" - -"@jest/expect-utils@^29.7.0": +"@jest/expect-utils@^29.5.0", "@jest/expect-utils@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== @@ -9969,16 +9962,6 @@ jest-config@^29.7.0: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.3.tgz#365c6b037ea8e67d2f2af68bc018fc18d44311f0" - integrity sha512-3sw+AdWnwH9sSNohMRKA7JiYUJSRr/WS6+sEFfBuhxU5V5GlEVKfvUn8JuMHE0wqKowemR1C2aHy8VtXbaV8dQ== - dependencies: - chalk "^4.0.0" - diff-sequences "^29.6.3" - jest-get-type "^29.6.3" - pretty-format "^29.6.3" - jest-diff@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" @@ -10033,7 +10016,7 @@ jest-environment-node@^29.7.0: jest-mock "^29.7.0" jest-util "^29.7.0" -jest-get-type@^29.6.3: +jest-get-type@^29.4.3, jest-get-type@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== @@ -10084,17 +10067,7 @@ jest-leak-detector@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" -jest-matcher-utils@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.3.tgz#a7574092b635d96a38fa0a22d015fb596b9c2efc" - integrity sha512-6ZrMYINZdwduSt5Xu18/n49O1IgXdjsfG7NEZaQws9k69eTKWKcVbJBw/MZsjOZe2sSyJFmuzh8042XWwl54Zg== - dependencies: - chalk "^4.0.0" - jest-diff "^29.6.3" - jest-get-type "^29.6.3" - pretty-format "^29.6.3" - -jest-matcher-utils@^29.7.0: +jest-matcher-utils@^29.5.0, jest-matcher-utils@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== @@ -10104,22 +10077,7 @@ jest-matcher-utils@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" -jest-message-util@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.3.tgz#bce16050d86801b165f20cfde34dc01d3cf85fbf" - integrity sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.6.3" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" - pretty-format "^29.6.3" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-message-util@^29.7.0: +jest-message-util@^29.5.0, jest-message-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== @@ -10264,7 +10222,7 @@ jest-snapshot@^29.7.0: pretty-format "^29.7.0" semver "^7.5.3" -jest-util@^29.0.0, jest-util@^29.7.0: +jest-util@^29.0.0, jest-util@^29.5.0, jest-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== @@ -10288,18 +10246,6 @@ jest-util@^29.6.0: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-util@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.3.tgz#e15c3eac8716440d1ed076f09bc63ace1aebca63" - integrity sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - jest-validate@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" @@ -12517,7 +12463,7 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-format@^29.0.0, pretty-format@^29.6.3: +pretty-format@^29.0.0: version "29.6.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.3.tgz#d432bb4f1ca6f9463410c3fb25a0ba88e594ace7" integrity sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw== From 1372f66a8a6f61977e3c64d5214c75c333838841 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Wed, 29 Nov 2023 15:08:24 +0100 Subject: [PATCH 03/55] feat: advanced flow with googlepay --- packages/lib/src/components/Dropin/Dropin.tsx | 2 + .../src/components/GooglePay/GooglePay.tsx | 57 ++++++++++++------- .../playground/src/pages/Dropin/manual.js | 10 +++- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/packages/lib/src/components/Dropin/Dropin.tsx b/packages/lib/src/components/Dropin/Dropin.tsx index 8b2a8b6525..72e0c95ace 100644 --- a/packages/lib/src/components/Dropin/Dropin.tsx +++ b/packages/lib/src/components/Dropin/Dropin.tsx @@ -123,6 +123,8 @@ class DropinElement extends UIElement<DropinElementProps> { }; public handleAction(action: PaymentAction, props = {}): this | null { + debugger; + if (!action || !action.type) { if (hasOwnProperty(action, 'action') && hasOwnProperty(action, 'resultCode')) { throw new Error( diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index df644f3c16..9cdec4b6e5 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -52,7 +52,7 @@ class GooglePay extends UIElement<GooglePayProps> { paymentMethod: { type: this.type, googlePayCardNetwork: this.state.googlePayCardNetwork, - googlePayToken: '{}' + googlePayToken: this.state.googlePayToken }, browserInfo: this.browserInfo }; @@ -88,38 +88,55 @@ class GooglePay extends UIElement<GooglePayProps> { return new Promise<google.payments.api.PaymentAuthorizationResult>(resolve => { super .submit() - .then(result => { - console.log('Resolving'); - + // TODO: add action.resolve type + .then((result: any) => { resolve({ transactionState: 'SUCCESS' }); - this.props.onPaymentCompleted(result, this.elementRef); + return result; + }) + .then(result => { + debugger; + if (result.action) { + this.elementRef.handleAction(result.action, result.actionProps); + return; + } + // + // // if (data.order) { + // // const { order, paymentMethodsResponse } = data; + // // // @ts-ignore Just testing + // // this.core.update({ paymentMethodsResponse, order, amount: data.order.remainingAmount }); + // // return; + // // } + // + this.handleFinalResult(result); }) - .catch(googlePayError => { - console.log('Caught error'); + .catch(error => { + this.setElementStatus('ready'); resolve({ transactionState: 'ERROR', error: { intent: 'PAYMENT_AUTHORIZATION', - message: googlePayError?.message || 'Something went wrong', - reason: googlePayError?.reason || 'OTHER_ERROR' + message: error?.googlePayError?.message || 'Something went wrong', + reason: error?.googlePayError?.reason || 'OTHER_ERROR' } }); }); }); }; - protected override throwPaymentMethodErrorIfNeeded = (error?: SubmitReject): never => { - this.setElementStatus('ready'); - - const googleError: google.payments.api.PaymentDataError = { - intent: 'PAYMENT_AUTHORIZATION', - message: error?.googlePayError?.message || 'Something went wrong', - reason: error?.googlePayError?.reason || 'OTHER_ERROR' - }; - - throw googleError; - }; + private override async submitUsingAdvancedFlow(): Promise<any> { + return new Promise((resolve, reject) => { + this.props.onSubmit( + { + data: this.data, + isValid: this.isValid, + ...(this.state.authorizedData && { authorizedData: this.state.authorizedData }) + }, + this.elementRef, + { resolve, reject } + ); + }); + } /** * Validation diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index e33136c979..cb99ead530 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -26,6 +26,8 @@ export async function initManual() { const result = await makePayment(state.data); + // actions.reject(); + // if (result.action) { actions.resolve({ action: result.action, @@ -55,9 +57,15 @@ export async function initManual() { if (result.resultCode === 'Authorised' || result.resultCode === 'Received') { actions.resolve(result); // DO I NEED FULL RESULT? } else { - actions.reject(result) + actions.reject(result); } + // return { + // googlePayError: { + // message: 'Not sufficient funds', + // reason: 'OTHER_ERROR,' + // } + // } // // // Trigger Error for GooglePay From 9a05b85252d9de884c10bc9b60b30e21cc6bee1f Mon Sep 17 00:00:00 2001 From: antoniof <m1aw@users.noreply.github.com> Date: Thu, 30 Nov 2023 11:04:10 +0100 Subject: [PATCH 04/55] draft gp sessions flow --- .../src/components/GooglePay/GooglePay.tsx | 92 +++++++++++++------ packages/lib/src/components/UIElement.tsx | 8 +- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 9cdec4b6e5..5c216c9224 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -1,5 +1,5 @@ import { h } from 'preact'; -import UIElement, { SubmitReject } from '../UIElement'; +import UIElement from '../UIElement'; import GooglePayService from './GooglePayService'; import GooglePayButton from './components/GooglePayButton'; import defaultProps from './defaultProps'; @@ -8,6 +8,9 @@ import { getGooglePayLocale } from './utils'; import collectBrowserInfo from '../../utils/browserInfo'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; +import { CheckoutSessionPaymentResponse } from '../../types'; +import { PaymentResponse } from '../types'; +import { resolveFinalResult } from '../utils'; class GooglePay extends UIElement<GooglePayProps> { public static type = TxVariants.googlepay; @@ -90,40 +93,66 @@ class GooglePay extends UIElement<GooglePayProps> { .submit() // TODO: add action.resolve type .then((result: any) => { - resolve({ transactionState: 'SUCCESS' }); - return result; - }) - .then(result => { - debugger; + // close for 3ds flow if (result.action) { - this.elementRef.handleAction(result.action, result.actionProps); - return; + resolve({ transactionState: 'SUCCESS' }); + return result; + } + }) + .then(async result => { + return this.handleOnPaymentAuthorizedResponse(result); + }) + .then(status => { + if (status && status === 'success') { + if (status == 'success') { + resolve({ transactionState: 'SUCCESS' }); + } else if (status == 'error') { + resolve({ + transactionState: 'ERROR', + error: { + intent: 'PAYMENT_AUTHORIZATION', + message: error?.googlePayError?.message || 'Something went wrong', + reason: error?.googlePayError?.reason || 'OTHER_ERROR' + } + }); + } } - // - // // if (data.order) { - // // const { order, paymentMethodsResponse } = data; - // // // @ts-ignore Just testing - // // this.core.update({ paymentMethodsResponse, order, amount: data.order.remainingAmount }); - // // return; - // // } - // - this.handleFinalResult(result); }) .catch(error => { this.setElementStatus('ready'); - - resolve({ - transactionState: 'ERROR', - error: { - intent: 'PAYMENT_AUTHORIZATION', - message: error?.googlePayError?.message || 'Something went wrong', - reason: error?.googlePayError?.reason || 'OTHER_ERROR' - } - }); + console.log(error); }); }); }; + // TODO types + private handleOnPaymentAuthorizedResponse = async result => { + // TODO check is best away to check for sessions/ + if (this.props.onSubmit) { + if (result.action) { + this.elementRef.handleAction(result.action, result.actionProps); + return; + } + return this.handleFinalResult(result); + } else { + return this.handleSessionsResponse(result); + } + }; + + protected handleFinalResult = (result: PaymentResponse) => { + const [status, statusProps] = resolveFinalResult(result); + + if (this.props.setStatusAutomatically && status) { + this.setElementStatus(status, statusProps); + } + + if (this.props.onPaymentCompleted) { + this.props.onPaymentCompleted(result, this.elementRef); + } + + return result; + }; + private override async submitUsingAdvancedFlow(): Promise<any> { return new Promise((resolve, reject) => { this.props.onSubmit( @@ -138,6 +167,17 @@ class GooglePay extends UIElement<GooglePayProps> { }); } + protected async makeSessionPaymentsCall(data): Promise<void> { + let paymentsResponse: CheckoutSessionPaymentResponse = null; + try { + paymentsResponse = await this.core.session.submitPayment(data); + } catch (error) { + // TODO resolve with error + this.handleError(error); + } + return paymentsResponse; + } + /** * Validation */ diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 75991529e0..6271ceabbd 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -196,7 +196,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten return; } - private async makeSessionPaymentsCall(data): Promise<void> { + protected async makeSessionPaymentsCall(data): Promise<void> { let paymentsResponse: CheckoutSessionPaymentResponse = null; try { @@ -316,7 +316,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten if (this.props.onPaymentCompleted) { this.props.onPaymentCompleted(result, this.elementRef); } - // return result; + return result; }; /** @@ -324,7 +324,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten * The component will handle automatically actions, orders, and final results. * @param rawResponse - */ - protected handleSessionsResponse(rawResponse: RawPaymentResponse): void { + protected handleSessionsResponse(rawResponse: RawPaymentResponse) { const response = getSanitizedResponse(rawResponse); if (response.action) { @@ -334,7 +334,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten // we do this way so the logic on handlingOrder is associated with payment method this.handleOrder(response); } else { - this.handleFinalResult(response); + return this.handleFinalResult(response); // this.elementRef.handleFinalResult(response); } } From 66354318e1eabf7c97b8abb006b92f17261dc734 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 30 Nov 2023 17:38:04 +0100 Subject: [PATCH 05/55] feat: googlepay adjustments --- .../src/components/GooglePay/GooglePay.tsx | 108 ++--------- packages/lib/src/components/UIElement.tsx | 180 ++++++++---------- packages/lib/src/core/types.ts | 16 +- .../playground/src/pages/Dropin/manual.js | 77 ++++---- 4 files changed, 161 insertions(+), 220 deletions(-) diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 5c216c9224..ec5c9ad81d 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -8,9 +8,8 @@ import { getGooglePayLocale } from './utils'; import collectBrowserInfo from '../../utils/browserInfo'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; -import { CheckoutSessionPaymentResponse } from '../../types'; import { PaymentResponse } from '../types'; -import { resolveFinalResult } from '../utils'; +import { onSubmitReject } from '../../core/types'; class GooglePay extends UIElement<GooglePayProps> { public static type = TxVariants.googlepay; @@ -61,12 +60,9 @@ class GooglePay extends UIElement<GooglePayProps> { }; } - public submit = () => { - return new Promise((resolve, reject) => this.props.onClick(resolve, reject)) + public override submit = () => { + new Promise((resolve, reject) => this.props.onClick(resolve, reject)) .then(() => this.googlePay.initiatePayment(this.props)) - .then(() => { - console.log('HERE'); - }) .catch((error: google.payments.api.PaymentsError) => { if (error.statusCode === 'CANCELED') { this.handleError(new AdyenCheckoutError('CANCEL', error.toString(), { cause: error })); @@ -89,95 +85,29 @@ class GooglePay extends UIElement<GooglePayProps> { }); return new Promise<google.payments.api.PaymentAuthorizationResult>(resolve => { - super - .submit() - // TODO: add action.resolve type - .then((result: any) => { - // close for 3ds flow - if (result.action) { - resolve({ transactionState: 'SUCCESS' }); - return result; - } - }) - .then(async result => { - return this.handleOnPaymentAuthorizedResponse(result); + this.makePaymentsCall() + .then((paymentResponse: PaymentResponse) => { + resolve({ transactionState: 'SUCCESS' }); + return paymentResponse; }) - .then(status => { - if (status && status === 'success') { - if (status == 'success') { - resolve({ transactionState: 'SUCCESS' }); - } else if (status == 'error') { - resolve({ - transactionState: 'ERROR', - error: { - intent: 'PAYMENT_AUTHORIZATION', - message: error?.googlePayError?.message || 'Something went wrong', - reason: error?.googlePayError?.reason || 'OTHER_ERROR' - } - }); - } - } + .then(async paymentResponse => { + this.handleResponse(paymentResponse); }) - .catch(error => { + .catch((error: onSubmitReject) => { this.setElementStatus('ready'); - console.log(error); + + resolve({ + transactionState: 'ERROR', + error: { + intent: error?.error?.googlePayError?.intent || 'PAYMENT_AUTHORIZATION', + message: error?.error?.googlePayError?.message || 'Something went wrong', + reason: error?.error?.googlePayError?.reason || 'OTHER_ERROR' + } + }); }); }); }; - // TODO types - private handleOnPaymentAuthorizedResponse = async result => { - // TODO check is best away to check for sessions/ - if (this.props.onSubmit) { - if (result.action) { - this.elementRef.handleAction(result.action, result.actionProps); - return; - } - return this.handleFinalResult(result); - } else { - return this.handleSessionsResponse(result); - } - }; - - protected handleFinalResult = (result: PaymentResponse) => { - const [status, statusProps] = resolveFinalResult(result); - - if (this.props.setStatusAutomatically && status) { - this.setElementStatus(status, statusProps); - } - - if (this.props.onPaymentCompleted) { - this.props.onPaymentCompleted(result, this.elementRef); - } - - return result; - }; - - private override async submitUsingAdvancedFlow(): Promise<any> { - return new Promise((resolve, reject) => { - this.props.onSubmit( - { - data: this.data, - isValid: this.isValid, - ...(this.state.authorizedData && { authorizedData: this.state.authorizedData }) - }, - this.elementRef, - { resolve, reject } - ); - }); - } - - protected async makeSessionPaymentsCall(data): Promise<void> { - let paymentsResponse: CheckoutSessionPaymentResponse = null; - try { - paymentsResponse = await this.core.session.submitPayment(data); - } catch (error) { - // TODO resolve with error - this.handleError(error); - } - return paymentsResponse; - } - /** * Validation */ diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 6271ceabbd..b9a0ac2314 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -1,7 +1,7 @@ import { h } from 'preact'; import BaseElement from './BaseElement'; -import { CheckoutSessionPaymentResponse, PaymentAction } from '../types'; -import { ComponentMethodsRef, PaymentResponse } from './types'; +import { CheckoutSessionDetailsResponse, CheckoutSessionPaymentResponse, PaymentAction } from '../types'; +import { ComponentMethodsRef, PaymentData, PaymentResponse } from './types'; import PayButton from './internal/PayButton'; import { IUIElement, PayButtonFunctionProps, RawPaymentResponse, UIElementProps } from './types'; import { getSanitizedResponse, resolveFinalResult } from './utils'; @@ -53,9 +53,11 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten this.makePaymentsCall = this.makePaymentsCall.bind(this); this.handleAction = this.handleAction.bind(this); this.handleOrder = this.handleOrder.bind(this); - this.handleSessionsResponse = this.handleSessionsResponse.bind(this); + this.handleResponse = this.handleResponse.bind(this); this.setElementStatus = this.setElementStatus.bind(this); + this.submitUsingSessionsFlow = this.submitUsingSessionsFlow.bind(this); + this.elementRef = (props && props.elementRef) || this; this.resources = this.props.modules ? this.props.modules.resources : undefined; @@ -92,9 +94,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten public setState(newState: object): void { this.state = { ...this.state, ...newState }; - - console.log('new state', this.state); - this.onChange(); } @@ -107,10 +106,26 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten return state; } + /** + * Submit payment method data. If the form is not valid, it will trigger validation. + */ + public submit(): void { + if (!this.isValid) { + this.showValidation(); + return; + } + + this.makePaymentsCall() + .then(this.handleResponse) + .catch(() => { + this.elementRef.setStatus('ready'); + }); + } + /** * Triggers the payment flow */ - private async makePaymentsCall(): Promise<void> { + protected makePaymentsCall(): Promise<PaymentResponse | CheckoutSessionPaymentResponse> { if (this.props.setStatusAutomatically) { this.setElementStatus('loading'); } @@ -120,93 +135,64 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten } if (this.core.session) { - return this.submitUsingSessionsFlow(); + const beforeSubmitEvent: Promise<PaymentData> = this.props.beforeSubmit + ? new Promise((resolve, reject) => + this.props.beforeSubmit(this.data, this.elementRef, { + resolve, + reject + }) + ) + : Promise.resolve(this.data); + + return beforeSubmitEvent.then(this.submitUsingSessionsFlow); } this.handleError(new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Could not submit the payment')); } - private async submitUsingAdvancedFlow() { - return ( - new Promise((resolve, reject) => { - this.props.onSubmit( - { - data: this.data, - isValid: this.isValid, - ...(this.state.authorizedData && { authorizedData: this.state.authorizedData }) - }, - this.elementRef, - { resolve, reject } - ); - }) - .then((data: any) => { - if (data.action) { - this.elementRef.handleAction(data.action, ...data.actionProps); - return; - } - if (data.order) { - const { order, paymentMethodsResponse } = data; - // @ts-ignore Just testing - this.core.update({ paymentMethodsResponse, order, amount: data.order.remainingAmount }); - return; - } - - this.handleFinalResult(data); - }) - // action.reject got called OR something fail above. TODO: add proper checks - .catch(error => { - this.throwPaymentMethodErrorIfNeeded(error); - }) - ); - } - - private async submitUsingSessionsFlow() { - const beforeSubmitEvent = this.props.beforeSubmit - ? new Promise((resolve, reject) => - this.props.beforeSubmit(this.data, this.elementRef, { - resolve, - reject - }) - ) - : Promise.resolve(this.data); - - let data; - - try { - data = await beforeSubmitEvent; - } catch { - // set state as ready to submit if the merchant cancels the action - this.elementRef.setStatus('ready'); - return; - } - - return this.makeSessionPaymentsCall(data); - } + private async submitUsingAdvancedFlow(): Promise<PaymentResponse> { + return new Promise<PaymentResponse>((resolve, reject) => { + this.props.onSubmit( + { + data: this.data, + isValid: this.isValid, + ...(this.state.authorizedData && { authorizedData: this.state.authorizedData }) + }, + this.elementRef, + { resolve, reject } + ); + }); - /** - * Method used to break the /payments flow and feed the error data back to the component in case the - * payment fails. - * - * Example: GooglePay / ApplePay accepts data from merchant in order to display custom errors - * - * @param error - Error object that can be passed back by the merchant - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected throwPaymentMethodErrorIfNeeded(error?: SubmitReject): void | never { - return; + // .then((paymentsResponse: PaymentResponse) => { + // // Handle result? + // if (paymentsResponse.action) { + // // @ts-ignore Fix props + // this.elementRef.handleAction(paymentsResponse.action, ...paymentsResponse.actionProps); + // return paymentsResponse; + // } + // if (paymentsResponse.order) { + // // @ts-ignore Just testing + // const { order, paymentMethodsResponse } = paymentsResponse; + // // @ts-ignore Just testing + // this.core.update({ paymentMethodsResponse, order, amount: data.order.remainingAmount }); + // return paymentsResponse; + // } + // + // this.handleFinalResult(paymentsResponse); + // return paymentsResponse; + // }); } - protected async makeSessionPaymentsCall(data): Promise<void> { + private async submitUsingSessionsFlow(data: PaymentData): Promise<CheckoutSessionPaymentResponse> { let paymentsResponse: CheckoutSessionPaymentResponse = null; try { paymentsResponse = await this.core.session.submitPayment(data); } catch (error) { this.handleError(error); - this.throwPaymentMethodErrorIfNeeded(); } - this.handleSessionsResponse(paymentsResponse); + return paymentsResponse; } private onValid() { @@ -219,18 +205,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten if (this.props.onComplete) this.props.onComplete(state, this.elementRef); } - /** - * Submit payment method data. If the form is not valid, it will trigger validation. - */ - public async submit(): Promise<void> { - if (!this.isValid) { - this.showValidation(); - return; - } - - return this.makePaymentsCall(); - } - public showValidation(): this { if (this.componentRef && this.componentRef.showValidation) this.componentRef.showValidation(); return this; @@ -248,8 +222,13 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten return this; } - private submitAdditionalDetails(data): Promise<void> { - return this.core.session.submitDetails(data).then(this.handleSessionsResponse).catch(this.handleError); + private async submitAdditionalDetails(data): Promise<void> { + try { + const response: CheckoutSessionDetailsResponse = await this.core.session.submitDetails(data); + this.handleResponse(response); + } catch (error) { + this.handleError(error); + } } protected handleError = (error: AdyenCheckoutError): void => { @@ -322,21 +301,28 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten /** * Handles a session /payments or /payments/details response. * The component will handle automatically actions, orders, and final results. + * * @param rawResponse - */ - protected handleSessionsResponse(rawResponse: RawPaymentResponse) { + protected handleResponse(rawResponse: RawPaymentResponse): void { const response = getSanitizedResponse(rawResponse); if (response.action) { this.elementRef.handleAction(response.action); - } else if (response.order?.remainingAmount?.value > 0) { + return; + } + + /** + * TODO: handle order properly on advanced flow. + */ + if (response.order?.remainingAmount?.value > 0) { // we don't want to call elementRef here, use the component handler // we do this way so the logic on handlingOrder is associated with payment method this.handleOrder(response); - } else { - return this.handleFinalResult(response); - // this.elementRef.handleFinalResult(response); + return; } + + this.handleFinalResult(response); } /** diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index d576585fbc..aac829d5f5 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -32,6 +32,13 @@ export interface ICore { export type AdyenEnvironment = 'test' | 'live' | 'live-us' | 'live-au' | 'live-apse' | 'live-in' | string; +export type onSubmitReject = { + error?: { + googlePayError?: Partial<google.payments.api.PaymentDataError>; + applePayError: {}; + }; +}; + export interface CoreOptions { session?: any; /** @@ -150,7 +157,14 @@ export interface CoreOptions { onPaymentCompleted?(data: OnPaymentCompletedData, element?: UIElement): void; - onSubmit?(state: any, element: UIElement): void; + onSubmit?( + state: any, + element: UIElement, + actions: { + resolve: () => void; + reject: (error: onSubmitReject) => void; + } + ): void; onAdditionalDetails?(state: any, element?: UIElement): void; diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index cb99ead530..6028d27df8 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -24,41 +24,52 @@ export async function initManual() { const { authorizedData } = state; console.log('authorizedData', authorizedData); - const result = await makePayment(state.data); - - // actions.reject(); - // - if (result.action) { - actions.resolve({ - action: result.action, - actionProps: { - challengeWindowSize: '01' - } - }); - return; - } - - if (result.order && result.order?.remainingAmount?.value > 0) { - const order = { - orderData: result.order.orderData, - pspReference: result.order.pspReference - }; - - const orderPaymentMethods = await getPaymentMethods({ order, amount, shopperLocale }); - - actions.resolve({ - order, - paymentMethodsResponse: orderPaymentMethods - }); - - return; + try { + const result = await makePayment(state.data); + + // happpy flow + if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { + action.reject({ + error: { + googlePayError: {}, + applePayError: {} + } + }); + } else { + actions.resolve({ + action: result.action, + order: result.order, + resultCode: result.resultCode + }); + } + } catch (error) { + // Something failed in the request + actions.reject(); } - if (result.resultCode === 'Authorised' || result.resultCode === 'Received') { - actions.resolve(result); // DO I NEED FULL RESULT? - } else { - actions.reject(result); - } + // + // + // if (result.order && result.order?.remainingAmount?.value > 0) { + // const order = { + // orderData: result.order.orderData, + // pspReference: result.order.pspReference + // }; + // + // const orderPaymentMethods = await getPaymentMethods({ order, amount, shopperLocale }); + // + // actions.resolve({ + // order, + // paymentMethodsResponse: orderPaymentMethods + // }); + // + // return; + // } + // + // if (result.resultCode === 'Authorised' || result.resultCode === 'Received') { + // actions.resolve(result); // DO I NEED FULL RESULT? + // } else { + // actions.reject(result); + // } // return { // googlePayError: { From d47f03a85f8f3fa12860791bb94d94ba9d17c1e7 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Fri, 1 Dec 2023 16:21:06 +0100 Subject: [PATCH 06/55] apple pay adjustments --- .../lib/src/components/ApplePay/ApplePay.tsx | 42 ++++++++++++++----- .../ApplePay/components/ApplePayButton.tsx | 2 +- packages/lib/src/components/ApplePay/types.ts | 2 - .../src/components/GooglePay/GooglePay.tsx | 2 +- packages/lib/src/components/UIElement.tsx | 5 +++ packages/lib/src/core/types.ts | 4 +- 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index f7e4b18073..d93179b500 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -8,9 +8,11 @@ import { httpPost } from '../../core/Services/http'; import { APPLEPAY_SESSION_ENDPOINT } from './config'; import { preparePaymentRequest } from './payment-request'; import { resolveSupportedVersion, mapBrands } from './utils'; -import { ApplePayElementProps, ApplePayElementData, ApplePaySessionRequest, OnAuthorizedCallback } from './types'; +import { ApplePayElementProps, ApplePayElementData, ApplePaySessionRequest } from './types'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; +import { PaymentResponse } from '../types'; +import { onSubmitReject } from '../../core/types'; const latestSupportedVersion = 14; @@ -48,17 +50,17 @@ class ApplePayElement extends UIElement<ApplePayElementProps> { return { paymentMethod: { type: ApplePayElement.type, - ...this.state + applePayToken: this.state.applePayToken } }; } - // @ts-ignore FIX THIS - public submit = async () => { - return this.startSession(this.props.onAuthorized); + public submit = (): void => { + this.startSession(); }; - private startSession(onPaymentAuthorized: OnAuthorizedCallback) { + // private startSession(onPaymentAuthorized: OnAuthorizedCallback) { + private startSession() { const { version, onValidateMerchant, onPaymentMethodSelected, onShippingMethodSelected, onShippingContactSelected } = this.props; const paymentRequest = preparePaymentRequest({ @@ -79,11 +81,29 @@ class ApplePayElement extends UIElement<ApplePayElementProps> { onShippingContactSelected, onValidateMerchant: onValidateMerchant || this.validateMerchant, onPaymentAuthorized: (resolve, reject, event) => { - if (event?.payment?.token?.paymentData) { - this.setState({ applePayToken: btoa(JSON.stringify(event.payment.token.paymentData)) }); - } - super.submit(); - onPaymentAuthorized(resolve, reject, event); + this.setState({ + applePayToken: btoa(JSON.stringify(event.payment.token.paymentData)) + }); + + this.makePaymentsCall() + .then((paymentResponse: PaymentResponse) => { + // check the order part here + + resolve(); + return paymentResponse; + }) + .then(paymentResponse => { + this.handleResponse(paymentResponse); + }) + .catch((error: onSubmitReject) => { + this.setElementStatus('ready'); + const errors = error?.error?.applePayError; + + reject({ + status: ApplePaySession.STATUS_FAILURE, + errors: errors ? (Array.isArray(errors) ? errors : [errors]) : undefined + }); + }); } }); diff --git a/packages/lib/src/components/ApplePay/components/ApplePayButton.tsx b/packages/lib/src/components/ApplePay/components/ApplePayButton.tsx index 02de70bba2..8f6d27be04 100644 --- a/packages/lib/src/components/ApplePay/components/ApplePayButton.tsx +++ b/packages/lib/src/components/ApplePay/components/ApplePayButton.tsx @@ -32,7 +32,7 @@ class ApplePayButton extends Component<ApplePayButtonProps> { 'apple-pay', 'apple-pay-button', `apple-pay-button-${buttonColor}`, - `apple-pay-button--type-add-money` + `apple-pay-button--type-${buttonType}` )} onClick={this.props.onClick} /> diff --git a/packages/lib/src/components/ApplePay/types.ts b/packages/lib/src/components/ApplePay/types.ts index eb633a0115..fe58c542dd 100644 --- a/packages/lib/src/components/ApplePay/types.ts +++ b/packages/lib/src/components/ApplePay/types.ts @@ -143,8 +143,6 @@ export interface ApplePayElementProps extends UIElementProps { onClick?: (resolve, reject) => void; - onAuthorized?: OnAuthorizedCallback; - onValidateMerchant?: (resolve, reject, validationURL: string) => void; /** diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index ec5c9ad81d..5c767597cb 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -90,7 +90,7 @@ class GooglePay extends UIElement<GooglePayProps> { resolve({ transactionState: 'SUCCESS' }); return paymentResponse; }) - .then(async paymentResponse => { + .then(paymentResponse => { this.handleResponse(paymentResponse); }) .catch((error: onSubmitReject) => { diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index b9a0ac2314..db2bdbc138 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -190,6 +190,11 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten paymentsResponse = await this.core.session.submitPayment(data); } catch (error) { this.handleError(error); + /** + * Re-throw the error, so this Promise gets rejected. This keeps the same behavior as the + * 'submitUsingAdvancedFlow' + */ + throw error; } return paymentsResponse; diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index aac829d5f5..a56c8cdcf7 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -35,7 +35,7 @@ export type AdyenEnvironment = 'test' | 'live' | 'live-us' | 'live-au' | 'live-a export type onSubmitReject = { error?: { googlePayError?: Partial<google.payments.api.PaymentDataError>; - applePayError: {}; + applePayError?: ApplePayJS.ApplePayError[] | ApplePayJS.ApplePayError; }; }; @@ -162,7 +162,7 @@ export interface CoreOptions { element: UIElement, actions: { resolve: () => void; - reject: (error: onSubmitReject) => void; + reject: (error?: onSubmitReject) => void; } ): void; From 76625a995873e8bfe7671179f5482ab7c68469c2 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Fri, 1 Dec 2023 17:02:58 +0100 Subject: [PATCH 07/55] feat: clean up --- .../src/components/CashAppPay/CashAppPay.tsx | 2 +- packages/lib/src/components/Dropin/Dropin.tsx | 4 +- packages/lib/src/components/PayPal/Paypal.tsx | 2 +- .../playground/src/pages/Dropin/manual.js | 46 ------------------- 4 files changed, 3 insertions(+), 51 deletions(-) diff --git a/packages/lib/src/components/CashAppPay/CashAppPay.tsx b/packages/lib/src/components/CashAppPay/CashAppPay.tsx index a27ef22ed3..86e9c6887a 100644 --- a/packages/lib/src/components/CashAppPay/CashAppPay.tsx +++ b/packages/lib/src/components/CashAppPay/CashAppPay.tsx @@ -93,7 +93,7 @@ export class CashAppPay extends UIElement<CashAppPayConfiguration> { return this.props.storedPaymentMethodId ? 'Cash App Pay' : ''; } - public submit = async () => { + public submit = () => { const { onClick, storedPaymentMethodId } = this.props; if (storedPaymentMethodId) { diff --git a/packages/lib/src/components/Dropin/Dropin.tsx b/packages/lib/src/components/Dropin/Dropin.tsx index 84751a3f6e..77f5097d5a 100644 --- a/packages/lib/src/components/Dropin/Dropin.tsx +++ b/packages/lib/src/components/Dropin/Dropin.tsx @@ -83,7 +83,7 @@ class DropinElement extends UIElement<DropinConfiguration> { /** * Calls the onSubmit event with the state of the activePaymentMethod */ - public async submit(): Promise<void> { + public override submit(): void { if (!this.activePaymentMethod) { throw new Error('No active payment method.'); } @@ -122,8 +122,6 @@ class DropinElement extends UIElement<DropinConfiguration> { }; public handleAction(action: PaymentAction, props = {}): this | null { - debugger; - if (!action || !action.type) { if (hasOwnProperty(action, 'action') && hasOwnProperty(action, 'resultCode')) { throw new Error( diff --git a/packages/lib/src/components/PayPal/Paypal.tsx b/packages/lib/src/components/PayPal/Paypal.tsx index 9c8091df33..55517faf0f 100644 --- a/packages/lib/src/components/PayPal/Paypal.tsx +++ b/packages/lib/src/components/PayPal/Paypal.tsx @@ -42,7 +42,7 @@ class PaypalElement extends UIElement<PayPalConfiguration> { }; } - public submit = async () => { + public submit = () => { this.handleError(new AdyenCheckoutError('IMPLEMENTATION_ERROR', ERRORS.SUBMIT_NOT_SUPPORTED)); }; diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index 6028d27df8..b73fae229d 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -16,7 +16,6 @@ export async function initManual() { locale: 'pt-BR', translationFile: getTranslationFile(shopperLocale), - // translationFile: nl_NL, environment: process.env.__CLIENT_ENV__, @@ -64,19 +63,6 @@ export async function initManual() { // // return; // } - // - // if (result.resultCode === 'Authorised' || result.resultCode === 'Received') { - // actions.resolve(result); // DO I NEED FULL RESULT? - // } else { - // actions.reject(result); - // } - - // return { - // googlePayError: { - // message: 'Not sufficient funds', - // reason: 'OTHER_ERROR,' - // } - // } // // // Trigger Error for GooglePay @@ -93,38 +79,6 @@ export async function initManual() { onPaymentCompleted(result, element) { console.log('onPaymentCompleted', result, element); }, - // - // - // // payments call - // if (result === AUTHORIZED) - // return action.resolve({ orderTrackng: { /// details }})} - // } - // - // - // action.reject(new ApplePayError(''))); - - // // handle actions - // if (result.action) { - // // demo only - store paymentData & order - // if (result.action.paymentData) localStorage.setItem('storedPaymentData', result.action.paymentData); - // component.handleAction(result.action, { challengeWindowSize: '01' }); - // } 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); - // } - // }, - // srConfig: { showPanel: true }, - // onChange: state => { - // console.log('onChange', state); - // }, onAdditionalDetails: async (state, component) => { const result = await makeDetailsCall(state.data); From 57561e947c4da1a0568a4680c2949ccf8ae703ca Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Mon, 4 Dec 2023 12:36:33 +0100 Subject: [PATCH 08/55] sanitizing response and checking resultCode --- .../lib/src/components/ApplePay/ApplePay.tsx | 2 + .../src/components/GooglePay/GooglePay.tsx | 2 + .../internal/UIElement/UIElement.tsx | 62 +++---- .../components/internal/UIElement/types.ts | 158 +++++++++--------- packages/lib/src/core/types.ts | 5 +- packages/lib/src/types/global-types.ts | 6 + .../playground/src/pages/Dropin/manual.js | 2 +- 7 files changed, 117 insertions(+), 120 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index 991fb9836b..71a02557e5 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -86,6 +86,8 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { }); this.makePaymentsCall() + .then(this.sanitizeResponse) + .then(this.verifyPaymentDidNotFail) .then((paymentResponse: PaymentResponseData) => { // check the order part here resolve(); diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index a5d41cf8c0..b520ee850e 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -90,6 +90,8 @@ class GooglePay extends UIElement<GooglePayConfiguration> { return new Promise<google.payments.api.PaymentAuthorizationResult>(resolve => { this.makePaymentsCall() + .then(this.sanitizeResponse) + .then(this.verifyPaymentDidNotFail) .then((paymentResponse: PaymentResponseData) => { resolve({ transactionState: 'SUCCESS' }); return paymentResponse; diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 1841aa95b8..a19c0736bd 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -8,21 +8,10 @@ import { CoreConfiguration, ICore } from '../../../core/types'; import { Resources } from '../../../core/Context/Resources'; import { NewableComponent } from '../../../core/core.registry'; import { ComponentMethodsRef, IUIElement, PayButtonFunctionProps, UIElementProps, UIElementStatus } from './types'; -import { PaymentAction, PaymentResponseData, PaymentData, RawPaymentResponse } from '../../../types/global-types'; +import { PaymentAction, PaymentResponseData, PaymentData, RawPaymentResponse, PaymentResponseAdvancedFlow } from '../../../types/global-types'; import './UIElement.scss'; import { CheckoutSessionPaymentResponse } from '../../../core/CheckoutSession/types'; -export type SubmitReject = { - googlePayError?: { - message?: string; - reason?: google.payments.api.ErrorReason; - }; - applePayError?: { - // TOOD - [key: string]: any; - }; -}; - export abstract class UIElement<P extends UIElementProps = UIElementProps> extends BaseElement<P> implements IUIElement { protected componentRef: any; @@ -115,6 +104,8 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten } this.makePaymentsCall() + .then(this.sanitizeResponse) + .then(this.verifyPaymentDidNotFail) .then(this.handleResponse) .catch(() => { this.elementRef.setStatus('ready'); @@ -124,7 +115,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten /** * Triggers the payment flow */ - protected makePaymentsCall(): Promise<PaymentResponseData | CheckoutSessionPaymentResponse> { + protected makePaymentsCall(): Promise<PaymentResponseAdvancedFlow | CheckoutSessionPaymentResponse> { if (this.props.setStatusAutomatically) { this.setElementStatus('loading'); } @@ -149,8 +140,8 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten this.handleError(new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Could not submit the payment')); } - private async submitUsingAdvancedFlow(): Promise<PaymentResponseData> { - return new Promise<PaymentResponseData>((resolve, reject) => { + private async submitUsingAdvancedFlow(): Promise<PaymentResponseAdvancedFlow> { + return new Promise<PaymentResponseAdvancedFlow>((resolve, reject) => { this.props.onSubmit( { data: this.data, @@ -161,25 +152,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten { resolve, reject } ); }); - - // .then((paymentsResponse: PaymentResponse) => { - // // Handle result? - // if (paymentsResponse.action) { - // // @ts-ignore Fix props - // this.elementRef.handleAction(paymentsResponse.action, ...paymentsResponse.actionProps); - // return paymentsResponse; - // } - // if (paymentsResponse.order) { - // // @ts-ignore Just testing - // const { order, paymentMethodsResponse } = paymentsResponse; - // // @ts-ignore Just testing - // this.core.update({ paymentMethodsResponse, order, amount: data.order.remainingAmount }); - // return paymentsResponse; - // } - // - // this.handleFinalResult(paymentsResponse); - // return paymentsResponse; - // }); } private async submitUsingSessionsFlow(data: PaymentData): Promise<CheckoutSessionPaymentResponse> { @@ -189,16 +161,28 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten paymentsResponse = await this.core.session.submitPayment(data); } catch (error) { this.handleError(error); - /** - * Re-throw the error, so this Promise gets rejected. This keeps the same behavior as the - * 'submitUsingAdvancedFlow' - */ - throw error; + return Promise.reject(error); } return paymentsResponse; } + protected sanitizeResponse(rawResponse: RawPaymentResponse): PaymentResponseData { + return getSanitizedResponse(rawResponse); + } + + protected verifyPaymentDidNotFail(response: PaymentResponseData): Promise<PaymentResponseData> { + // Testing response with Refused + response.resultCode = 'Refused'; + + const [status] = resolveFinalResult(response); + + if (status !== 'error') { + return Promise.resolve(response); + } + return Promise.reject(); + } + private onValid() { const state = { data: this.data }; if (this.props.onValid) this.props.onValid(state, this.elementRef); diff --git a/packages/lib/src/components/internal/UIElement/types.ts b/packages/lib/src/components/internal/UIElement/types.ts index 1d326ab11e..e58beda21d 100644 --- a/packages/lib/src/components/internal/UIElement/types.ts +++ b/packages/lib/src/components/internal/UIElement/types.ts @@ -9,84 +9,86 @@ import { CoreConfiguration, ICore } from '../../../core/types'; export type PayButtonFunctionProps = Omit<PayButtonProps, 'amount'>; -export interface UIElementProps extends BaseElementProps { - environment?: string; - session?: Session; - onChange?: (state: any, element: UIElement) => void; - onValid?: (state: any, element: UIElement) => void; - beforeSubmit?: (state: any, element: UIElement, actions: any) => Promise<void>; - // TODO: fix actions type - onSubmit?: (state: any, element: UIElement, actions: any) => void; - onComplete?: (state, element: UIElement) => void; - onActionHandled?: (rtnObj: ActionHandledReturnObject) => void; - onAdditionalDetails?: (state: any, element: UIElement) => void; - onError?: (error, element?: UIElement) => void; - onPaymentCompleted?: (result: any, element: UIElement) => void; - beforeRedirect?: (resolve, reject, redirectData, element: UIElement) => void; - - isInstantPayment?: boolean; - - /** - * Flags if the element is Stored payment method - * @internal - */ - isStoredPaymentMethod?: boolean; - - /** - * Flag if the element is Stored payment method. - * Perhaps can be deprecated and we use the one above? - * @internal - */ - oneClick?: boolean; - - /** - * Stored payment method id - * @internal - */ - storedPaymentMethodId?: string; - - /** - * Status set when creating the Component from action - * @internal - */ - statusType?: 'redirect' | 'loading' | 'custom'; - - type?: string; - name?: string; - icon?: string; - amount?: PaymentAmount; - secondaryAmount?: PaymentAmountExtended; - - /** - * Show/Hide pay button - * @defaultValue true - */ - showPayButton?: boolean; - - /** - * Set to false to not set the Component status to 'loading' when onSubmit is triggered. - * @defaultValue true - */ - setStatusAutomatically?: boolean; - - /** @internal */ - payButton?: (options: PayButtonFunctionProps) => h.JSX.Element; - - /** @internal */ - loadingContext?: string; - - /** @internal */ - createFromAction?: (action: PaymentAction, props: object) => UIElement; - - /** @internal */ - clientKey?: string; - - /** @internal */ - elementRef?: any; - - /** @internal */ - i18n?: Language; -} +type CoreCallbacks = Pick<CoreConfiguration, 'onSubmit'>; + +export type UIElementProps = BaseElementProps & + CoreCallbacks & { + environment?: string; + session?: Session; + onChange?: (state: any, element: UIElement) => void; + onValid?: (state: any, element: UIElement) => void; + beforeSubmit?: (state: any, element: UIElement, actions: any) => Promise<void>; + + onComplete?: (state, element: UIElement) => void; + onActionHandled?: (rtnObj: ActionHandledReturnObject) => void; + onAdditionalDetails?: (state: any, element: UIElement) => void; + onError?: (error, element?: UIElement) => void; + onPaymentCompleted?: (result: any, element: UIElement) => void; + beforeRedirect?: (resolve, reject, redirectData, element: UIElement) => void; + + isInstantPayment?: boolean; + + /** + * Flags if the element is Stored payment method + * @internal + */ + isStoredPaymentMethod?: boolean; + + /** + * Flag if the element is Stored payment method. + * Perhaps can be deprecated and we use the one above? + * @internal + */ + oneClick?: boolean; + + /** + * Stored payment method id + * @internal + */ + storedPaymentMethodId?: string; + + /** + * Status set when creating the Component from action + * @internal + */ + statusType?: 'redirect' | 'loading' | 'custom'; + + type?: string; + name?: string; + icon?: string; + amount?: PaymentAmount; + secondaryAmount?: PaymentAmountExtended; + + /** + * Show/Hide pay button + * @defaultValue true + */ + showPayButton?: boolean; + + /** + * Set to false to not set the Component status to 'loading' when onSubmit is triggered. + * @defaultValue true + */ + setStatusAutomatically?: boolean; + + /** @internal */ + payButton?: (options: PayButtonFunctionProps) => h.JSX.Element; + + /** @internal */ + loadingContext?: string; + + /** @internal */ + createFromAction?: (action: PaymentAction, props: object) => UIElement; + + /** @internal */ + clientKey?: string; + + /** @internal */ + elementRef?: any; + + /** @internal */ + i18n?: Language; + }; export interface IUIElement extends IBaseElement { isValid: boolean; diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 6a1c1fb15c..76625d9af8 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -6,7 +6,8 @@ import { PaymentMethodsResponse, ActionHandledReturnObject, OnPaymentCompletedData, - PaymentData + PaymentData, + PaymentResponseAdvancedFlow } from '../types/global-types'; import { AnalyticsOptions } from './Analytics/types'; import { RiskModuleOptions } from './RiskModule/RiskModule'; @@ -168,7 +169,7 @@ export interface CoreConfiguration { state: any, element: UIElement, actions: { - resolve: () => void; + resolve: (response: PaymentResponseAdvancedFlow) => void; reject: (error?: onSubmitReject) => void; } ): void; diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index 1e9aaa5077..9c9beb86b8 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -335,6 +335,12 @@ export interface OnPaymentCompletedData { resultCode: ResultCode; } +export interface PaymentResponseAdvancedFlow { + resultCode: ResultCode; + action?: PaymentAction; + order?: Order; +} + export interface PaymentResponseData { type?: string; action?: PaymentAction; diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index b73fae229d..070608b2c5 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -28,7 +28,7 @@ export async function initManual() { // happpy flow if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { - action.reject({ + actions.reject({ error: { googlePayError: {}, applePayError: {} From fde4db9792cc850c5a33f8055103f3c27fac42c6 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Mon, 4 Dec 2023 14:27:54 +0100 Subject: [PATCH 09/55] fixed bug checking resultCode --- .../internal/UIElement/UIElement.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index a19c0736bd..978d43c213 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -108,7 +108,11 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten .then(this.verifyPaymentDidNotFail) .then(this.handleResponse) .catch(() => { - this.elementRef.setStatus('ready'); + // two scenarios when code reaches here: + // - adv flow: merchant used reject passing error back or empty object + // - adv flow: merchant resolved passed resultCode: Refused,Cancelled,etc + // - on sessions, payment failed with resultCode Refused, Cancelled, etc + this.setElementStatus('ready'); }); } @@ -172,15 +176,11 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten } protected verifyPaymentDidNotFail(response: PaymentResponseData): Promise<PaymentResponseData> { - // Testing response with Refused - response.resultCode = 'Refused'; - - const [status] = resolveFinalResult(response); - - if (status !== 'error') { - return Promise.resolve(response); + if (['Cancelled', 'Error', 'Refused'].includes(response.resultCode)) { + return Promise.reject(); } - return Promise.reject(); + + return Promise.resolve(response); } private onValid() { From b5c8862cc40db769498c4087cf4a91996d31f273 Mon Sep 17 00:00:00 2001 From: antoniof <m1aw@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:33:55 +0100 Subject: [PATCH 10/55] add: onPaymentFailed callback --- .../internal/UIElement/UIElement.tsx | 29 ++++++++++------ .../components/internal/UIElement/types.ts | 3 +- packages/lib/src/core/config.ts | 1 + packages/lib/src/core/types.ts | 5 ++- packages/lib/src/types/global-types.ts | 7 ++++ packages/playground/src/handlers.js | 33 ++++++++++++++----- packages/playground/src/pages/Cards/Cards.js | 6 ++++ .../playground/src/pages/Dropin/manual.js | 4 +++ .../playground/src/pages/Dropin/session.js | 3 ++ 9 files changed, 70 insertions(+), 21 deletions(-) diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 978d43c213..2854351ab8 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -1,14 +1,21 @@ import { h } from 'preact'; import BaseElement from '../BaseElement/BaseElement'; import PayButton from '../PayButton'; -import { getSanitizedResponse, resolveFinalResult } from './utils'; +import { getSanitizedResponse } from './utils'; import AdyenCheckoutError from '../../../core/Errors/AdyenCheckoutError'; import { hasOwnProperty } from '../../../utils/hasOwnProperty'; import { CoreConfiguration, ICore } from '../../../core/types'; import { Resources } from '../../../core/Context/Resources'; import { NewableComponent } from '../../../core/core.registry'; import { ComponentMethodsRef, IUIElement, PayButtonFunctionProps, UIElementProps, UIElementStatus } from './types'; -import { PaymentAction, PaymentResponseData, PaymentData, RawPaymentResponse, PaymentResponseAdvancedFlow } from '../../../types/global-types'; +import { + PaymentAction, + PaymentResponseData, + PaymentData, + RawPaymentResponse, + PaymentResponseAdvancedFlow, + OnPaymentFailedData +} from '../../../types/global-types'; import './UIElement.scss'; import { CheckoutSessionPaymentResponse } from '../../../core/CheckoutSession/types'; @@ -107,12 +114,15 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten .then(this.sanitizeResponse) .then(this.verifyPaymentDidNotFail) .then(this.handleResponse) - .catch(() => { + .catch((exception: OnPaymentFailedData) => { // two scenarios when code reaches here: // - adv flow: merchant used reject passing error back or empty object // - adv flow: merchant resolved passed resultCode: Refused,Cancelled,etc - // - on sessions, payment failed with resultCode Refused, Cancelled, etc - this.setElementStatus('ready'); + // - on sessions, payment failed with resultCode Refused, Cancelled, etcthis. + this.props.onPaymentFailed?.(exception, this.elementRef); + if (this.props.setStatusAutomatically) { + this.setElementStatus('error'); + } }); } @@ -177,7 +187,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten protected verifyPaymentDidNotFail(response: PaymentResponseData): Promise<PaymentResponseData> { if (['Cancelled', 'Error', 'Refused'].includes(response.resultCode)) { - return Promise.reject(); + return Promise.reject(response); } return Promise.resolve(response); @@ -281,10 +291,9 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten if (this.props.onPaymentCompleted) this.props.onPaymentCompleted(response, this.elementRef); }; - protected handleFinalResult = (result: PaymentResponseData) => { + protected handleSuccessResult = (result: PaymentResponseData) => { if (this.props.setStatusAutomatically) { - const [status, statusProps] = resolveFinalResult(result); - if (status) this.setElementStatus(status, statusProps); + this.setElementStatus('success'); } if (this.props.onPaymentCompleted) { @@ -317,7 +326,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten return; } - this.handleFinalResult(response); + this.handleSuccessResult(response); } /** diff --git a/packages/lib/src/components/internal/UIElement/types.ts b/packages/lib/src/components/internal/UIElement/types.ts index e58beda21d..c10faa9689 100644 --- a/packages/lib/src/components/internal/UIElement/types.ts +++ b/packages/lib/src/components/internal/UIElement/types.ts @@ -9,7 +9,8 @@ import { CoreConfiguration, ICore } from '../../../core/types'; export type PayButtonFunctionProps = Omit<PayButtonProps, 'amount'>; -type CoreCallbacks = Pick<CoreConfiguration, 'onSubmit'>; +// TODO add onPaymentCompleted +type CoreCallbacks = Pick<CoreConfiguration, 'onSubmit' | 'onPaymentFailed'>; export type UIElementProps = BaseElementProps & CoreCallbacks & { diff --git a/packages/lib/src/core/config.ts b/packages/lib/src/core/config.ts index b51f719a5a..1b8aa91996 100644 --- a/packages/lib/src/core/config.ts +++ b/packages/lib/src/core/config.ts @@ -19,6 +19,7 @@ export const GENERIC_OPTIONS = [ // Events 'onPaymentCompleted', + 'onPaymentFailed', 'beforeRedirect', 'beforeSubmit', 'onSubmit', diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 76625d9af8..a7387eba0a 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -7,7 +7,8 @@ import { ActionHandledReturnObject, OnPaymentCompletedData, PaymentData, - PaymentResponseAdvancedFlow + PaymentResponseAdvancedFlow, + OnPaymentFailedData } from '../types/global-types'; import { AnalyticsOptions } from './Analytics/types'; import { RiskModuleOptions } from './RiskModule/RiskModule'; @@ -165,6 +166,8 @@ export interface CoreConfiguration { onPaymentCompleted?(data: OnPaymentCompletedData, element?: UIElement): void; + onPaymentFailed?(data?: OnPaymentFailedData, element?: UIElement): void; + onSubmit?( state: any, element: UIElement, diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index 9c9beb86b8..bdb98e6678 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -1,5 +1,6 @@ import { ADDRESS_SCHEMA } from '../components/internal/Address/constants'; import actionTypes from '../core/ProcessResponse/PaymentAction/actionTypes'; +import { onSubmitReject } from '../core/types'; export type PaymentActionsType = keyof typeof actionTypes; @@ -335,6 +336,12 @@ export interface OnPaymentCompletedData { resultCode: ResultCode; } +export type OnPaymentFailedData = + | OnPaymentCompletedData + | (onSubmitReject & { + resultCode: ResultCode; + }); + export interface PaymentResponseAdvancedFlow { resultCode: ResultCode; action?: PaymentAction; diff --git a/packages/playground/src/handlers.js b/packages/playground/src/handlers.js index 6abe14c848..71a527100b 100644 --- a/packages/playground/src/handlers.js +++ b/packages/playground/src/handlers.js @@ -29,17 +29,32 @@ export function handleError(obj) { } } -export function handleSubmit(state, component) { +export async function handleSubmit(state, component, actions) { component.setStatus('loading'); - return makePayment(state.data) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); - }); + try { + const result = await makePayment(state.data); + + // happpy flow + if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { + actions.reject({ + resultCode: result.resultCode, + error: { + googlePayError: {}, + applePayError: {} + } + }); + } else { + actions.resolve({ + action: result.action, + order: result.order, + resultCode: result.resultCode + }); + } + } catch (error) { + // Something failed in the request + actions.reject(); + } } export function handleAdditionalDetails(details, component) { diff --git a/packages/playground/src/pages/Cards/Cards.js b/packages/playground/src/pages/Cards/Cards.js index d65c7fc89f..598378f354 100644 --- a/packages/playground/src/pages/Cards/Cards.js +++ b/packages/playground/src/pages/Cards/Cards.js @@ -45,6 +45,12 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = onError: handleError, risk: { enabled: false + }, + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); } }); diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index 070608b2c5..7690fb50ce 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -29,6 +29,7 @@ export async function initManual() { // happpy flow if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { actions.reject({ + resultCode: result.resultCode, error: { googlePayError: {}, applePayError: {} @@ -79,6 +80,9 @@ export async function initManual() { onPaymentCompleted(result, element) { console.log('onPaymentCompleted', result, element); }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + }, onAdditionalDetails: async (state, component) => { const result = await makeDetailsCall(state.data); diff --git a/packages/playground/src/pages/Dropin/session.js b/packages/playground/src/pages/Dropin/session.js index 08012e6cd2..e80d853e58 100644 --- a/packages/playground/src/pages/Dropin/session.js +++ b/packages/playground/src/pages/Dropin/session.js @@ -30,6 +30,9 @@ export async function initSession() { onPaymentCompleted: (result, component) => { console.info(result, component); }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + }, onError: (error, component) => { console.info(JSON.stringify(error), component); }, From 62f4518d447fd77ff3068cf08258c992ae4976f6 Mon Sep 17 00:00:00 2001 From: vagrant <m1aw@users.noreply.github.com> Date: Wed, 6 Dec 2023 04:41:03 -0600 Subject: [PATCH 11/55] refactor: change handle order, adds onPaymentMethodsRequest --- packages/lib/src/components/ANCV/ANCV.tsx | 16 -------- .../lib/src/components/Giftcard/Giftcard.tsx | 9 +---- .../internal/UIElement/UIElement.tsx | 39 +++++++++++++++++-- .../components/internal/UIElement/types.ts | 2 +- packages/lib/src/core/config.ts | 3 +- packages/lib/src/core/types.ts | 16 +++++++- packages/lib/src/types/global-types.ts | 8 ++++ 7 files changed, 61 insertions(+), 32 deletions(-) diff --git a/packages/lib/src/components/ANCV/ANCV.tsx b/packages/lib/src/components/ANCV/ANCV.tsx index 2cae6954a3..66be096e2d 100644 --- a/packages/lib/src/components/ANCV/ANCV.tsx +++ b/packages/lib/src/components/ANCV/ANCV.tsx @@ -8,8 +8,6 @@ import SRPanelProvider from '../../core/Errors/SRPanelProvider'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import PayButton from '../internal/PayButton'; import { ANCVConfiguration } from './types'; -import { PaymentResponseData } from '../../types/global-types'; - export class ANCVElement extends UIElement<ANCVConfiguration> { public static type = 'ancv'; @@ -36,20 +34,6 @@ export class ANCVElement extends UIElement<ANCVConfiguration> { } }; - /** - * Called when the /paymentDetails endpoint returns PartiallyAuthorised. The /paymentDetails happens once the /status - * returns PartiallyAuthorised - * - * @param order - */ - protected handleOrder = ({ order }: PaymentResponseData) => { - this.updateParent({ order }); - - if (this.props.session && this.props.onOrderCreated) { - return this.props.onOrderCreated(order); - } - }; - public createOrder = () => { if (!this.isValid) { this.showValidation(); diff --git a/packages/lib/src/components/Giftcard/Giftcard.tsx b/packages/lib/src/components/Giftcard/Giftcard.tsx index 76e0abef19..527a1dd5fa 100644 --- a/packages/lib/src/components/Giftcard/Giftcard.tsx +++ b/packages/lib/src/components/Giftcard/Giftcard.tsx @@ -4,7 +4,7 @@ import GiftcardComponent from './components/GiftcardComponent'; import CoreProvider from '../../core/Context/CoreProvider'; import PayButton from '../internal/PayButton'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; -import { PaymentAmount, PaymentResponseData } from '../../types//global-types'; +import { PaymentAmount } from '../../types//global-types'; import { GiftCardElementData, GiftCardConfiguration } from './types'; import { TxVariants } from '../tx-variants'; @@ -68,13 +68,6 @@ export class GiftcardElement extends UIElement<GiftCardConfiguration> { } }; - protected handleOrder = ({ order }: PaymentResponseData) => { - this.updateParent({ order }); - if (this.props.session && this.props.onOrderCreated) { - return this.props.onOrderCreated(order); - } - }; - public balanceCheck() { return this.onBalanceCheck(); } diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 2854351ab8..277c0362ac 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -14,7 +14,9 @@ import { PaymentData, RawPaymentResponse, PaymentResponseAdvancedFlow, - OnPaymentFailedData + OnPaymentFailedData, + PaymentMethodsResponse, + Order } from '../../../types/global-types'; import './UIElement.scss'; import { CheckoutSessionPaymentResponse } from '../../../core/CheckoutSession/types'; @@ -286,9 +288,16 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten } protected handleOrder = (response: PaymentResponseData): void => { - this.updateParent({ order: response.order }); - // in case we receive an order in any other component then a GiftCard trigger handleFinalResult - if (this.props.onPaymentCompleted) this.props.onPaymentCompleted(response, this.elementRef); + const { order } = response; + + const updateCorePromise = + !this.props.session && this.props.onPaymentMethodsRequest + ? this.handleAdvanceFlowPaymentMethodsUpdate(order) + : this.core.update({ order }); + + updateCorePromise.then(() => { + this.props.onOrderCreated?.({ order }); + }); }; protected handleSuccessResult = (result: PaymentResponseData) => { @@ -384,6 +393,28 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten protected payButton = (props: PayButtonFunctionProps) => { return <PayButton {...props} amount={this.props.amount} secondaryAmount={this.props.secondaryAmount} onClick={this.submit} />; }; + + private async handleAdvanceFlowPaymentMethodsUpdate(order: Order) { + return new Promise<PaymentMethodsResponse>((resolve, reject) => { + const data = { + order: { + orderData: order.orderData, + pspReference: order.pspReference + }, + amount: this.props.amount, + locale: this.core.options.locale + }; + this.props.onPaymentMethodsRequest(resolve, reject, data); + }) + .then(paymentMethodsResponse => { + return this.core.update({ paymentMethodsResponse, order, amount: order.remainingAmount }); + }) + .catch(error => { + this.handleError( + new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Payment methods be updated after partial payment.', { cause: error }) + ); + }); + } } export default UIElement; diff --git a/packages/lib/src/components/internal/UIElement/types.ts b/packages/lib/src/components/internal/UIElement/types.ts index c10faa9689..d4a358f012 100644 --- a/packages/lib/src/components/internal/UIElement/types.ts +++ b/packages/lib/src/components/internal/UIElement/types.ts @@ -10,7 +10,7 @@ import { CoreConfiguration, ICore } from '../../../core/types'; export type PayButtonFunctionProps = Omit<PayButtonProps, 'amount'>; // TODO add onPaymentCompleted -type CoreCallbacks = Pick<CoreConfiguration, 'onSubmit' | 'onPaymentFailed'>; +type CoreCallbacks = Pick<CoreConfiguration, 'onSubmit' | 'onPaymentFailed' | 'onOrderCreated' | 'onPaymentMethodsRequest'>; export type UIElementProps = BaseElementProps & CoreCallbacks & { diff --git a/packages/lib/src/core/config.ts b/packages/lib/src/core/config.ts index 1b8aa91996..64690d1631 100644 --- a/packages/lib/src/core/config.ts +++ b/packages/lib/src/core/config.ts @@ -31,7 +31,8 @@ export const GENERIC_OPTIONS = [ 'onBalanceCheck', 'onOrderRequest', 'onOrderCreated', - 'setStatusAutomatically' + 'setStatusAutomatically', + 'onPaymentMethodsRequest' ]; export default { diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index a7387eba0a..16d8c50258 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -8,7 +8,8 @@ import { OnPaymentCompletedData, PaymentData, PaymentResponseAdvancedFlow, - OnPaymentFailedData + OnPaymentFailedData, + PaymentMethodsRequestData } from '../types/global-types'; import { AnalyticsOptions } from './Analytics/types'; import { RiskModuleOptions } from './RiskModule/RiskModule'; @@ -26,14 +27,23 @@ type PromiseReject = typeof Promise.reject; export interface ICore { initialize(): Promise<ICore>; + register(...items: NewableComponent[]): void; - update({ order }: { order?: Order }): Promise<ICore>; + + update(options: CoreConfiguration): Promise<ICore>; + remove(component): ICore; + submitDetails(details: any): void; + getCorePropsForComponent(): any; + getComponent(txVariant: string): NewableComponent | undefined; + createFromAction(action: PaymentAction, options: any): any; + storeElementReference(element: UIElement): void; + options: CoreConfiguration; paymentMethodsResponse: PaymentMethods; session?: Session; @@ -189,6 +199,8 @@ export interface CoreConfiguration { onOrderRequest?(resolve: PromiseResolve, reject: PromiseReject, data: PaymentData): Promise<void>; + onPaymentMethodsRequest?(resolve: (response: PaymentMethodsResponse) => void, reject: () => void, data: PaymentMethodsRequestData): void; + onOrderCancel?(order: Order): void; /** diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index bdb98e6678..8364a7b589 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -342,6 +342,14 @@ export type OnPaymentFailedData = resultCode: ResultCode; }); +//TODO double check these values +export interface PaymentMethodsRequestData { + order: Order; + amount: PaymentAmount; + locale: string; + countryCode?: string; +} + export interface PaymentResponseAdvancedFlow { resultCode: ResultCode; action?: PaymentAction; From 4cf1e54fea1658c5d7ea0f1e314cd9daa77534b2 Mon Sep 17 00:00:00 2001 From: antoniof <m1aw@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:40:43 +0100 Subject: [PATCH 12/55] refactor: rename onOrderCreated to onOrderUpdated --- .../e2e-playwright/app/src/pages/ANCV/ANCV.js | 2 +- .../e2e-playwright/tests/ancv/ancv.spec.ts | 2 +- .../GiftCardsSessions/GiftCardsSessions.js | 4 ++-- .../onOrderUpdated.clientScripts.js} | 0 .../onOrderUpdated.mocks.js} | 0 .../onOrderUpdated.test.js} | 0 .../onRequiringConfirmation.mocks.js | 2 +- packages/lib/src/components/ANCV/types.ts | 2 +- packages/lib/src/components/Giftcard/types.ts | 2 +- .../internal/UIElement/UIElement.tsx | 5 ++--- .../components/internal/UIElement/types.ts | 2 +- packages/lib/src/core/config.ts | 2 +- packages/lib/src/core/types.ts | 11 +++++++---- packages/lib/src/types/global-types.ts | 5 ++--- .../playground/src/pages/Dropin/manual.js | 19 ++++++++++++++++--- .../src/pages/GiftCards/GiftCards.js | 4 ++-- packages/playground/src/services.js | 6 +++++- 17 files changed, 43 insertions(+), 25 deletions(-) rename packages/e2e/tests/giftcards/{onOrderCreated/onOrderCreated.clientScripts.js => onOrderUpdated/onOrderUpdated.clientScripts.js} (100%) rename packages/e2e/tests/giftcards/{onOrderCreated/onOrderCreated.mocks.js => onOrderUpdated/onOrderUpdated.mocks.js} (100%) rename packages/e2e/tests/giftcards/{onOrderCreated/onOrderCreated.test.js => onOrderUpdated/onOrderUpdated.test.js} (100%) diff --git a/packages/e2e-playwright/app/src/pages/ANCV/ANCV.js b/packages/e2e-playwright/app/src/pages/ANCV/ANCV.js index a43bcff1d5..5c0c7df19e 100644 --- a/packages/e2e-playwright/app/src/pages/ANCV/ANCV.js +++ b/packages/e2e-playwright/app/src/pages/ANCV/ANCV.js @@ -27,7 +27,7 @@ const initCheckout = async () => { locale: shopperLocale, countryCode, showPayButton: true, - onOrderCreated: data => { + onOrderUpdated: data => { showAuthorised('Partially Authorised'); }, onError: handleError diff --git a/packages/e2e-playwright/tests/ancv/ancv.spec.ts b/packages/e2e-playwright/tests/ancv/ancv.spec.ts index bdc56da0d8..a806f52949 100644 --- a/packages/e2e-playwright/tests/ancv/ancv.spec.ts +++ b/packages/e2e-playwright/tests/ancv/ancv.spec.ts @@ -11,7 +11,7 @@ import { setupMock } from '../../mocks/setup/setup.mock'; import { statusMock } from '../../mocks/status/status.mock'; test.describe('ANCV - Sessions', () => { - test('should call onOrderCreated when payment is partially authorised (Sessions flow)', async ({ ancvPage }) => { + test('should call onOrderUpdated when payment is partially authorised (Sessions flow)', async ({ ancvPage }) => { const { ancv, page } = ancvPage; await createOrderMock(page, orderCreatedMockData); diff --git a/packages/e2e/app/src/pages/GiftCardsSessions/GiftCardsSessions.js b/packages/e2e/app/src/pages/GiftCardsSessions/GiftCardsSessions.js index f6c12f812f..796980bfa1 100644 --- a/packages/e2e/app/src/pages/GiftCardsSessions/GiftCardsSessions.js +++ b/packages/e2e/app/src/pages/GiftCardsSessions/GiftCardsSessions.js @@ -37,8 +37,8 @@ const initCheckout = async () => { core: window.sessionCheckout, type: 'giftcard', brand: 'valuelink', - onOrderCreated: data => { - window.onOrderCreatedTestData = data; + onOrderUpdated: data => { + window.onOrderUpdatedTestData = data; }, onRequiringConfirmation: () => { window.onRequiringConfirmationTestData = true; diff --git a/packages/e2e/tests/giftcards/onOrderCreated/onOrderCreated.clientScripts.js b/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.clientScripts.js similarity index 100% rename from packages/e2e/tests/giftcards/onOrderCreated/onOrderCreated.clientScripts.js rename to packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.clientScripts.js diff --git a/packages/e2e/tests/giftcards/onOrderCreated/onOrderCreated.mocks.js b/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.mocks.js similarity index 100% rename from packages/e2e/tests/giftcards/onOrderCreated/onOrderCreated.mocks.js rename to packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.mocks.js diff --git a/packages/e2e/tests/giftcards/onOrderCreated/onOrderCreated.test.js b/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js similarity index 100% rename from packages/e2e/tests/giftcards/onOrderCreated/onOrderCreated.test.js rename to packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js diff --git a/packages/e2e/tests/giftcards/onRequiringConfirmation/onRequiringConfirmation.mocks.js b/packages/e2e/tests/giftcards/onRequiringConfirmation/onRequiringConfirmation.mocks.js index 2cdf353281..28daf29ae5 100644 --- a/packages/e2e/tests/giftcards/onRequiringConfirmation/onRequiringConfirmation.mocks.js +++ b/packages/e2e/tests/giftcards/onRequiringConfirmation/onRequiringConfirmation.mocks.js @@ -1,7 +1,7 @@ import { RequestMock, RequestLogger } from 'testcafe'; import { BASE_URL } from '../../pages'; -import { mock, loggers } from '../onOrderCreated/onOrderCreated.mocks'; +import { mock, loggers } from '../onOrderUpdated/onOrderUpdated.mocks'; const path = require('path'); require('dotenv').config({ path: path.resolve('../../', '.env') }); diff --git a/packages/lib/src/components/ANCV/types.ts b/packages/lib/src/components/ANCV/types.ts index a500dfd92d..4621bf55f0 100644 --- a/packages/lib/src/components/ANCV/types.ts +++ b/packages/lib/src/components/ANCV/types.ts @@ -4,7 +4,7 @@ export interface ANCVConfiguration extends UIElementProps { paymentData?: any; data: ANCVDataState; onOrderRequest?: any; - onOrderCreated?: any; + onOrderUpdated?: any; } export interface ANCVDataState { diff --git a/packages/lib/src/components/Giftcard/types.ts b/packages/lib/src/components/Giftcard/types.ts index 36f5c7cf35..f9976e5296 100644 --- a/packages/lib/src/components/Giftcard/types.ts +++ b/packages/lib/src/components/Giftcard/types.ts @@ -17,7 +17,7 @@ export interface GiftCardConfiguration extends UIElementProps { expiryDateRequired?: boolean; brandsConfiguration?: any; brand?: string; - onOrderCreated?(data): void; + onOrderUpdated?(data): void; onOrderRequest?(resolve, reject, data): void; onBalanceCheck?(resolve, reject, data): void; onRequiringConfirmation?(): void; diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 277c0362ac..d9cebea2a8 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -296,7 +296,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten : this.core.update({ order }); updateCorePromise.then(() => { - this.props.onOrderCreated?.({ order }); + this.props.onOrderUpdated?.({ order }); }); }; @@ -401,10 +401,9 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten orderData: order.orderData, pspReference: order.pspReference }, - amount: this.props.amount, locale: this.core.options.locale }; - this.props.onPaymentMethodsRequest(resolve, reject, data); + this.props.onPaymentMethodsRequest(data, { resolve, reject }); }) .then(paymentMethodsResponse => { return this.core.update({ paymentMethodsResponse, order, amount: order.remainingAmount }); diff --git a/packages/lib/src/components/internal/UIElement/types.ts b/packages/lib/src/components/internal/UIElement/types.ts index d4a358f012..d5db40edbb 100644 --- a/packages/lib/src/components/internal/UIElement/types.ts +++ b/packages/lib/src/components/internal/UIElement/types.ts @@ -10,7 +10,7 @@ import { CoreConfiguration, ICore } from '../../../core/types'; export type PayButtonFunctionProps = Omit<PayButtonProps, 'amount'>; // TODO add onPaymentCompleted -type CoreCallbacks = Pick<CoreConfiguration, 'onSubmit' | 'onPaymentFailed' | 'onOrderCreated' | 'onPaymentMethodsRequest'>; +type CoreCallbacks = Pick<CoreConfiguration, 'onSubmit' | 'onPaymentFailed' | 'onOrderUpdated' | 'onPaymentMethodsRequest'>; export type UIElementProps = BaseElementProps & CoreCallbacks & { diff --git a/packages/lib/src/core/config.ts b/packages/lib/src/core/config.ts index 64690d1631..4b2daf6564 100644 --- a/packages/lib/src/core/config.ts +++ b/packages/lib/src/core/config.ts @@ -30,7 +30,7 @@ export const GENERIC_OPTIONS = [ 'onError', 'onBalanceCheck', 'onOrderRequest', - 'onOrderCreated', + 'onOrderUpdated', 'setStatusAutomatically', 'onPaymentMethodsRequest' ]; diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 16d8c50258..b4d7b098d9 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -199,16 +199,19 @@ export interface CoreConfiguration { onOrderRequest?(resolve: PromiseResolve, reject: PromiseReject, data: PaymentData): Promise<void>; - onPaymentMethodsRequest?(resolve: (response: PaymentMethodsResponse) => void, reject: () => void, data: PaymentMethodsRequestData): void; + onPaymentMethodsRequest?( + data: PaymentMethodsRequestData, + actions: { resolve: (response: PaymentMethodsResponse) => void; reject: () => void } + ): void; onOrderCancel?(order: Order): void; /** - * Only used in Components combined with Sessions flow - * Callback used to inform when the order is created. + * Called when the gift card balance is less than the transaction amount. + * Returns an Order object that includes the remaining amount to be paid. * https://docs.adyen.com/payment-methods/gift-cards/web-component?tab=config-sessions_1 */ - onOrderCreated?(data: { order: Order }): void; + onOrderUpdated?(data: { order: Order }): void; /** * Used only in the Donation Component when shopper declines to donate diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index 8364a7b589..a76289dbb7 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -344,9 +344,8 @@ export type OnPaymentFailedData = //TODO double check these values export interface PaymentMethodsRequestData { - order: Order; - amount: PaymentAmount; - locale: string; + order?: Order; + locale?: string; countryCode?: string; } diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index 7690fb50ce..e4744ccc7e 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -1,7 +1,7 @@ import { AdyenCheckout, Dropin, Card, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; import { getPaymentMethods, makePayment, checkBalance, createOrder, cancelOrder, makeDetailsCall } from '../../services'; -import { amount, shopperLocale, countryCode, returnUrl } from '../../config/commonConfig'; +import { amount, shopperLocale, countryCode } from '../../config/commonConfig'; import { getSearchParameters } from '../../utils'; import getTranslationFile from '../../config/getTranslation'; @@ -102,20 +102,33 @@ export async function initManual() { } }, onBalanceCheck: async (resolve, reject, data) => { + console.log('onBalanceCheck', data); resolve(await checkBalance(data)); }, - onOrderRequest: async (resolve, reject) => { + onOrderRequest: async resolve => { + console.log('onOrderRequested'); resolve(await createOrder({ amount })); }, + onOrderUpdated: data => { + console.log('onOrderUpdated', data); + }, onOrderCancel: async order => { await cancelOrder(order); - checkout.update({ paymentMethodsResponse: await getPaymentMethods({ amount, shopperLocale }), order: null, amount }); + checkout.update({ + paymentMethodsResponse: await getPaymentMethods({ amount, shopperLocale }), + order: null, + amount + }); }, onError: (error, component) => { console.info(error.name, error.message, error.stack, component); }, onActionHandled: rtnObj => { console.log('onActionHandled', rtnObj); + }, + onPaymentMethodsRequest: async (data, { resolve, reject }) => { + console.log('onPaymentMethodsRequest', data); + resolve(await getPaymentMethods({ amount, shopperLocale: data.locale, order: data.order })); } }); diff --git a/packages/playground/src/pages/GiftCards/GiftCards.js b/packages/playground/src/pages/GiftCards/GiftCards.js index 2ecd10d1b7..4ea568be34 100644 --- a/packages/playground/src/pages/GiftCards/GiftCards.js +++ b/packages/playground/src/pages/GiftCards/GiftCards.js @@ -96,8 +96,8 @@ import getTranslationFile from '../../config/getTranslation'; core: sessionCheckout, type: 'giftcard', brand: 'svs', - onOrderCreated: () => { - console.log('onOrderCreated'); + onOrderUpdated: () => { + console.log('onOrderUpdated'); }, onRequiringConfirmation: () => { console.log('onRequiringConfirmation'); diff --git a/packages/playground/src/services.js b/packages/playground/src/services.js index 340b8eac9a..0ba6dd2554 100644 --- a/packages/playground/src/services.js +++ b/packages/playground/src/services.js @@ -54,7 +54,11 @@ 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 = { + ...data, + amount: paymentMethodsConfig.amount + }; + return httpPost('paymentMethods/balance', payload) .then(response => { if (response.error) throw 'Balance call failed'; return response; From 95519d72452f943348b81d733880bd2b8f8426b0 Mon Sep 17 00:00:00 2001 From: antoniof <m1aw@users.noreply.github.com> Date: Wed, 6 Dec 2023 17:22:52 +0100 Subject: [PATCH 13/55] fix: handle reject onPaymentMethodsRequest --- .prettierrc.json | 2 +- .../internal/UIElement/UIElement.tsx | 63 +++++++++++++------ .../playground/src/pages/Dropin/manual.js | 28 ++++++++- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index 3f1d5d07ce..f3ca00a53b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,6 +3,6 @@ "bracketSpacing": true, "trailingComma": "none", "tabWidth": 4, - "printWidth": 150, + "printWidth": 120, "singleQuote": true } diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index d9cebea2a8..0816ca7d83 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -21,7 +21,10 @@ import { import './UIElement.scss'; import { CheckoutSessionPaymentResponse } from '../../../core/CheckoutSession/types'; -export abstract class UIElement<P extends UIElementProps = UIElementProps> extends BaseElement<P> implements IUIElement { +export abstract class UIElement<P extends UIElementProps = UIElementProps> + extends BaseElement<P> + implements IUIElement +{ protected componentRef: any; protected resources: Resources; @@ -290,10 +293,9 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten protected handleOrder = (response: PaymentResponseData): void => { const { order } = response; - const updateCorePromise = - !this.props.session && this.props.onPaymentMethodsRequest - ? this.handleAdvanceFlowPaymentMethodsUpdate(order) - : this.core.update({ order }); + const updateCorePromise = this.core.session + ? this.core.update({ order }) + : this.handleAdvanceFlowPaymentMethodsUpdate(order); updateCorePromise.then(() => { this.props.onOrderUpdated?.({ order }); @@ -369,7 +371,9 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten * Get the element's displayable name */ public get displayName(): string { - const paymentMethodFromResponse = this.core.paymentMethodsResponse?.paymentMethods?.find(pm => pm.type === this.type); + const paymentMethodFromResponse = this.core.paymentMethodsResponse?.paymentMethods?.find( + pm => pm.type === this.type + ); return this.props.name || paymentMethodFromResponse?.name || this.type; } @@ -391,27 +395,50 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten * Get the payButton component for the current element */ protected payButton = (props: PayButtonFunctionProps) => { - return <PayButton {...props} amount={this.props.amount} secondaryAmount={this.props.secondaryAmount} onClick={this.submit} />; + return ( + <PayButton + {...props} + amount={this.props.amount} + secondaryAmount={this.props.secondaryAmount} + onClick={this.submit} + /> + ); }; private async handleAdvanceFlowPaymentMethodsUpdate(order: Order) { return new Promise<PaymentMethodsResponse>((resolve, reject) => { - const data = { - order: { - orderData: order.orderData, - pspReference: order.pspReference + if (!this.props.onPaymentMethodsRequest) { + return reject(); + } + + this.props.onPaymentMethodsRequest( + { + order: { + orderData: order.orderData, + pspReference: order.pspReference + }, + locale: this.core.options.locale }, - locale: this.core.options.locale - }; - this.props.onPaymentMethodsRequest(data, { resolve, reject }); + { resolve, reject } + ); }) - .then(paymentMethodsResponse => { - return this.core.update({ paymentMethodsResponse, order, amount: order.remainingAmount }); - }) .catch(error => { this.handleError( - new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Payment methods be updated after partial payment.', { cause: error }) + new AdyenCheckoutError( + 'IMPLEMENTATION_ERROR', + 'Something failed during payment methods update or onPaymentMethodsRequest was not implemented', + { + cause: error + } + ) ); + }) + .then(paymentMethodsResponse => { + return this.core.update({ + ...(paymentMethodsResponse && { paymentMethodsResponse }), + order, + amount: order.remainingAmount + }); }); } } diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index e4744ccc7e..7e092aa8cf 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -1,6 +1,24 @@ -import { AdyenCheckout, Dropin, Card, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay } from '@adyen/adyen-web'; +import { + AdyenCheckout, + Dropin, + Card, + GooglePay, + PayPal, + Ach, + Affirm, + WeChat, + Giftcard, + AmazonPay +} from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { getPaymentMethods, makePayment, checkBalance, createOrder, cancelOrder, makeDetailsCall } from '../../services'; +import { + getPaymentMethods, + makePayment, + checkBalance, + createOrder, + cancelOrder, + makeDetailsCall +} from '../../services'; import { amount, shopperLocale, countryCode } from '../../config/commonConfig'; import { getSearchParameters } from '../../utils'; import getTranslationFile from '../../config/getTranslation'; @@ -96,7 +114,11 @@ export async function initManual() { }; const orderPaymentMethods = await getPaymentMethods({ order, amount, shopperLocale }); - checkout.update({ paymentMethodsResponse: orderPaymentMethods, order, amount: result.order.remainingAmount }); + checkout.update({ + paymentMethodsResponse: orderPaymentMethods, + order, + amount: result.order.remainingAmount + }); } else { handleFinalState(result.resultCode, component); } From 39a92c93f39681c0e5809265cc2e83f9b1fe15fa Mon Sep 17 00:00:00 2001 From: vagrant <m1aw@users.noreply.github.com> Date: Thu, 7 Dec 2023 09:22:01 -0600 Subject: [PATCH 14/55] fixes UIelement props overwritten on update --- .../internal/BaseElement/BaseElement.ts | 8 ++- .../components/internal/UIElement/utils.ts | 3 +- packages/lib/src/core/core.ts | 29 +++++--- .../helpers/create-advanced-checkout.ts | 19 +++++- .../stories/giftcards/Gifcards.stories.tsx | 67 +++++++++++++++++++ 5 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 packages/lib/storybook/stories/giftcards/Gifcards.stories.tsx diff --git a/packages/lib/src/components/internal/BaseElement/BaseElement.ts b/packages/lib/src/components/internal/BaseElement/BaseElement.ts index 5bc03bf696..7b675b54b2 100644 --- a/packages/lib/src/components/internal/BaseElement/BaseElement.ts +++ b/packages/lib/src/components/internal/BaseElement/BaseElement.ts @@ -66,7 +66,9 @@ class BaseElement<P extends BaseElementProps> implements IBaseElement { public get data(): PaymentData { const clientData = getProp(this.props, 'modules.risk.data'); const useAnalytics = !!getProp(this.props, 'modules.analytics.props.enabled'); - const checkoutAttemptId = useAnalytics ? getProp(this.props, 'modules.analytics.checkoutAttemptId') : 'do-not-track'; + const checkoutAttemptId = useAnalytics + ? getProp(this.props, 'modules.analytics.checkoutAttemptId') + : 'do-not-track'; const order = this.state.order || this.props.order; const componentData = this.formatData(); @@ -126,8 +128,8 @@ class BaseElement<P extends BaseElementProps> implements IBaseElement { * @param props - props to update * @returns this - the element instance */ - public update(props: P): this { - this.buildElementProps({ ...this.props, ...props }); + public update(props: Partial<P>): this { + this.props = this.formatProps({ ...this.props, ...props }); this.state = {}; return this.unmount().mount(this._node); // for new mount fny diff --git a/packages/lib/src/components/internal/UIElement/utils.ts b/packages/lib/src/components/internal/UIElement/utils.ts index ace5355f64..7af89b9ff8 100644 --- a/packages/lib/src/components/internal/UIElement/utils.ts +++ b/packages/lib/src/components/internal/UIElement/utils.ts @@ -15,7 +15,8 @@ export function getSanitizedResponse(response: RawPaymentResponse): PaymentRespo return acc; }, {}); - if (removedProperties.length) console.warn(`The following properties should not be passed to the client: ${removedProperties.join(', ')}`); + if (removedProperties.length) + console.warn(`The following properties should not be passed to the client: ${removedProperties.join(', ')}`); return sanitizedObject as PaymentResponseData; } diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 8e43602d02..7817addeaf 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -57,12 +57,18 @@ class Core implements ICore { this.setOptions(props); this.loadingContext = resolveEnvironment(this.options.environment, this.options.environmentUrls?.api); - this.cdnContext = resolveCDNEnvironment(this.options.resourceEnvironment || this.options.environment, this.options.environmentUrls?.api); - this.session = this.options.session && new Session(this.options.session, this.options.clientKey, this.loadingContext); + this.cdnContext = resolveCDNEnvironment( + this.options.resourceEnvironment || this.options.environment, + this.options.environmentUrls?.api + ); + this.session = + this.options.session && new Session(this.options.session, this.options.clientKey, this.loadingContext); const clientKeyType = this.options.clientKey?.substr(0, 4); if ((clientKeyType === 'test' || clientKeyType === 'live') && !this.loadingContext.includes(clientKeyType)) { - throw new Error(`Error: you are using a '${clientKeyType}' clientKey against the '${this.options.environment}' environment`); + throw new Error( + `Error: you are using a '${clientKeyType}' clientKey against the '${this.options.environment}' environment` + ); } // Expose version number for npm builds @@ -135,7 +141,9 @@ class Core implements ICore { 'a "resultCode": have you passed in the whole response object by mistake?' ); } - throw new Error('createFromAction::Invalid Action - the passed action object does not have a "type" property'); + throw new Error( + 'createFromAction::Invalid Action - the passed action object does not have a "type" property' + ); } if (action.type) { @@ -156,12 +164,13 @@ class Core implements ICore { * @param options - props to update * @returns this - the element instance */ - public update = (options: CoreConfiguration = {}): Promise<this> => { + public update = (options: Partial<CoreConfiguration> = {}): Promise<this> => { this.setOptions(options); return this.initialize().then(() => { // Update each component under this instance - this.components.forEach(c => c.update(this.getCorePropsForComponent())); + // here we should update only the new options that have been received from core + this.components.forEach(c => c.update(options)); return this; }); }; @@ -224,7 +233,8 @@ class Core implements ICore { * @internal */ private handleCreateError(paymentMethod?): never { - const paymentMethodName = paymentMethod && paymentMethod.name ? paymentMethod.name : 'The passed payment method'; + const paymentMethodName = + paymentMethod && paymentMethod.name ? paymentMethod.name : 'The passed payment method'; const errorMessage = paymentMethod ? `${paymentMethodName} is not a valid Checkout Component. What was passed as a txVariant was: ${JSON.stringify( paymentMethod @@ -235,7 +245,10 @@ class Core implements ICore { } private createPaymentMethodsList(paymentMethodsResponse?: PaymentMethods): void { - this.paymentMethodsResponse = new PaymentMethods(this.options.paymentMethodsResponse || paymentMethodsResponse, this.options); + this.paymentMethodsResponse = new PaymentMethods( + this.options.paymentMethodsResponse || paymentMethodsResponse, + this.options + ); } private createCoreModules(): void { diff --git a/packages/lib/storybook/helpers/create-advanced-checkout.ts b/packages/lib/storybook/helpers/create-advanced-checkout.ts index 0af291ab9c..fda07a31b4 100644 --- a/packages/lib/storybook/helpers/create-advanced-checkout.ts +++ b/packages/lib/storybook/helpers/create-advanced-checkout.ts @@ -6,13 +6,22 @@ import { AdyenCheckoutProps } from '../stories/types'; import Checkout from '../../src/core/core'; import { PaymentMethodsResponse } from '../../src/types'; -async function createAdvancedFlowCheckout({ showPayButton, countryCode, shopperLocale, amount }: AdyenCheckoutProps): Promise<Checkout> { +async function createAdvancedFlowCheckout({ + showPayButton, + countryCode, + shopperLocale, + amount +}: AdyenCheckoutProps): Promise<Checkout> { const paymentAmount = { currency: getCurrency(countryCode), value: Number(amount) }; - const paymentMethodsResponse: PaymentMethodsResponse = await getPaymentMethods({ amount: paymentAmount, shopperLocale, countryCode }); + const paymentMethodsResponse: PaymentMethodsResponse = await getPaymentMethods({ + amount: paymentAmount, + shopperLocale, + countryCode + }); const checkout = await AdyenCheckout({ clientKey: process.env.CLIENT_KEY, @@ -41,8 +50,12 @@ async function createAdvancedFlowCheckout({ showPayButton, countryCode, shopperL }, onBalanceCheck: async (resolve, reject, data) => { + const payload = { + amount: paymentAmount, + ...data + }; try { - const res = await checkBalance(data); + const res = await checkBalance(payload); resolve(res); } catch (e) { reject(e); diff --git a/packages/lib/storybook/stories/giftcards/Gifcards.stories.tsx b/packages/lib/storybook/stories/giftcards/Gifcards.stories.tsx new file mode 100644 index 0000000000..49f07220f1 --- /dev/null +++ b/packages/lib/storybook/stories/giftcards/Gifcards.stories.tsx @@ -0,0 +1,67 @@ +import { Meta, StoryObj } from '@storybook/preact'; +import { PaymentMethodStoryProps } from '../types'; +import { getStoryContextCheckout } from '../../utils/get-story-context-checkout'; +import { Container } from '../Container'; +import { ANCVConfiguration } from '../../../src/components/ANCV/types'; +import Giftcard from '../../../src/components/Giftcard'; +import { GiftCardConfiguration } from '../../../src/components/Giftcard/types'; +import { makePayment } from '../../helpers/checkout-api-calls'; + +type GifcardStory = StoryObj<PaymentMethodStoryProps<GiftCardConfiguration>>; + +const meta: Meta<PaymentMethodStoryProps<ANCVConfiguration>> = { + title: 'Giftcards/Generic Giftcard' +}; + +export const Default: GifcardStory = { + render: (args, context) => { + const { componentConfiguration } = args; + const checkout = getStoryContextCheckout(context); + const ancv = new Giftcard({ core: checkout, ...componentConfiguration }); + return <Container element={ancv} />; + }, + args: { + countryCode: 'NL', + amount: 200000, + useSessions: false, + componentConfiguration: { + brand: 'genericgiftcard', + onSubmit: async (state, element, actions) => { + try { + const paymentData = { + amount: { + value: 200000, + currency: 'EUR' + }, + countryCode: 'NL', + shopperLocale: 'en-GB' + }; + const result = await makePayment(state.data, paymentData); + + // happpy flow + if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { + actions.reject({ + error: { + googlePayError: {} + } + }); + } else { + actions.resolve({ + action: result.action, + order: result.order, + resultCode: result.resultCode + }); + } + } catch (error) { + // Something failed in the request + actions.reject(); + } + }, + onOrderUpdated(data) { + // TODO render another component + alert(JSON.stringify(data)); + } + } + } +}; +export default meta; From ad6b2b532cdee067e1678c9145ac83a7a354eaf3 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Fri, 8 Dec 2023 14:53:27 -0300 Subject: [PATCH 15/55] feat: applepay order tracking untested changes --- packages/lib/package.json | 4 +- .../lib/src/components/ApplePay/ApplePay.tsx | 80 ++++++++++++++++--- .../components/ApplePay/ApplePayService.ts | 37 +++++---- packages/lib/src/components/ApplePay/types.ts | 29 +++++-- yarn.lock | 23 +++--- 5 files changed, 127 insertions(+), 46 deletions(-) diff --git a/packages/lib/package.json b/packages/lib/package.json index 19bfe50df9..ac39561f46 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -140,8 +140,8 @@ }, "dependencies": { "@adyen/bento-design-tokens": "0.5.4", - "@types/applepayjs": "3.0.4", - "@types/googlepay": "0.7.0", + "@types/applepayjs": "14.0.3", + "@types/googlepay": "0.7.5", "classnames": "2.3.1", "preact": "10.13.2" }, diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index 71a02557e5..c413d479b6 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -8,7 +8,12 @@ import { httpPost } from '../../core/Services/http'; import { APPLEPAY_SESSION_ENDPOINT } from './config'; import { preparePaymentRequest } from './payment-request'; import { resolveSupportedVersion, mapBrands } from './utils'; -import { ApplePayConfiguration, ApplePayElementData, ApplePaySessionRequest } from './types'; +import { + ApplePayConfiguration, + ApplePayElementData, + ApplePayPaymentOrderDetails, + ApplePaySessionRequest +} from './types'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; import { onSubmitReject } from '../../core/types'; @@ -56,12 +61,17 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { } public submit = (): void => { - this.startSession(); + void this.startSession(); }; - // private startSession(onPaymentAuthorized: OnAuthorizedCallback) { private startSession() { - const { version, onValidateMerchant, onPaymentMethodSelected, onShippingMethodSelected, onShippingContactSelected } = this.props; + const { + version, + onValidateMerchant, + onPaymentMethodSelected, + onShippingMethodSelected, + onShippingContactSelected + } = this.props; const paymentRequest = preparePaymentRequest({ companyName: this.props.configuration.merchantName, @@ -71,7 +81,11 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { const session = new ApplePayService(paymentRequest, { version, onError: (error: unknown) => { - this.handleError(new AdyenCheckoutError('ERROR', 'ApplePay - Something went wrong on ApplePayService', { cause: error })); + this.handleError( + new AdyenCheckoutError('ERROR', 'ApplePay - Something went wrong on ApplePayService', { + cause: error + }) + ); }, onCancel: event => { this.handleError(new AdyenCheckoutError('CANCEL', 'ApplePay UI dismissed', { cause: event })); @@ -88,9 +102,12 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { this.makePaymentsCall() .then(this.sanitizeResponse) .then(this.verifyPaymentDidNotFail) - .then((paymentResponse: PaymentResponseData) => { - // check the order part here - resolve(); + .then(this.collectOrderTrackingDetailsIfNeeded) + .then(({ paymentResponse, orderDetails }) => { + resolve({ + status: ApplePaySession.STATUS_SUCCESS, + ...(orderDetails && { orderDetails }) + }); return paymentResponse; }) .then(paymentResponse => { @@ -117,13 +134,45 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { })); } + /** + * Verify if the 'onOrderTrackingRequest' is provided. If so, triggers the callback expecting an + * Apple Pay order details back + * + * @private + */ + private async collectOrderTrackingDetailsIfNeeded( + paymentResponse: PaymentResponseData + ): Promise<{ orderDetails?: ApplePayPaymentOrderDetails; paymentResponse: PaymentResponseData }> { + return new Promise<ApplePayPaymentOrderDetails | void>((resolve, reject) => { + if (!this.props.onOrderTrackingRequest) { + return resolve(); + } + + this.props.onOrderTrackingRequest(resolve, reject); + }) + .then(orderDetails => { + return { + paymentResponse, + ...(orderDetails && { orderDetails }) + }; + }) + .catch(() => { + return { paymentResponse }; + }); + } + private async validateMerchant(resolve, reject) { const { hostname: domainName } = window.location; const { clientKey, configuration, loadingContext, initiative } = this.props; const { merchantName, merchantId } = configuration; const path = `${APPLEPAY_SESSION_ENDPOINT}?clientKey=${clientKey}`; const options = { loadingContext, path }; - const request: ApplePaySessionRequest = { displayName: merchantName, domainName, initiative, merchantIdentifier: merchantId }; + const request: ApplePaySessionRequest = { + displayName: merchantName, + domainName, + initiative, + merchantIdentifier: merchantId + }; try { const response = await httpPost(options, request); @@ -152,7 +201,12 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { */ public override async isAvailable(): Promise<void> { if (document.location.protocol !== 'https:') { - return Promise.reject(new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Trying to start an Apple Pay session from an insecure document')); + return Promise.reject( + new AdyenCheckoutError( + 'IMPLEMENTATION_ERROR', + 'Trying to start an Apple Pay session from an insecure document' + ) + ); } if (!this.props.onValidateMerchant && !this.props.clientKey) { @@ -160,7 +214,11 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { } try { - if (window.ApplePaySession && ApplePaySession.canMakePayments() && ApplePaySession.supportsVersion(this.props.version)) { + if ( + window.ApplePaySession && + ApplePaySession.canMakePayments() && + ApplePaySession.supportsVersion(this.props.version) + ) { return Promise.resolve(); } } catch (error) { diff --git a/packages/lib/src/components/ApplePay/ApplePayService.ts b/packages/lib/src/components/ApplePay/ApplePayService.ts index 891fef6c09..eb27517007 100644 --- a/packages/lib/src/components/ApplePay/ApplePayService.ts +++ b/packages/lib/src/components/ApplePay/ApplePayService.ts @@ -1,4 +1,4 @@ -import { OnAuthorizedCallback } from './types'; +import { ApplePayPaymentAuthorizationResult } from './types'; interface ApplePayServiceOptions { version: number; @@ -8,7 +8,11 @@ interface ApplePayServiceOptions { onPaymentMethodSelected?: (resolve, reject, event: ApplePayJS.ApplePayPaymentMethodSelectedEvent) => void; onShippingMethodSelected?: (resolve, reject, event: ApplePayJS.ApplePayShippingMethodSelectedEvent) => void; onShippingContactSelected?: (resolve, reject, event: ApplePayJS.ApplePayShippingContactSelectedEvent) => void; - onPaymentAuthorized?: OnAuthorizedCallback; + onPaymentAuthorized: ( + resolve: (result: ApplePayPaymentAuthorizationResult) => void, + reject: (result: ApplePayPaymentAuthorizationResult) => void, + event: ApplePayJS.ApplePayPaymentAuthorizedEvent + ) => void; } class ApplePayService { @@ -24,15 +28,18 @@ class ApplePayService { this.session.oncancel = event => this.oncancel(event, options.onCancel); if (typeof options.onPaymentMethodSelected === 'function') { - this.session.onpaymentmethodselected = event => this.onpaymentmethodselected(event, options.onPaymentMethodSelected); + this.session.onpaymentmethodselected = event => + this.onpaymentmethodselected(event, options.onPaymentMethodSelected); } if (typeof options.onShippingContactSelected === 'function') { - this.session.onshippingcontactselected = event => this.onshippingcontactselected(event, options.onShippingContactSelected); + this.session.onshippingcontactselected = event => + this.onshippingcontactselected(event, options.onShippingContactSelected); } if (typeof options.onShippingMethodSelected === 'function') { - this.session.onshippingmethodselected = event => this.onshippingmethodselected(event, options.onShippingMethodSelected); + this.session.onshippingmethodselected = event => + this.onshippingmethodselected(event, options.onShippingMethodSelected); } } @@ -72,19 +79,16 @@ class ApplePayService { * @param onPaymentAuthorized - A promise that will complete the payment when resolved. Use this promise to process the payment. * @see {@link https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778020-onpaymentauthorized} */ - onpaymentauthorized(event: ApplePayJS.ApplePayPaymentAuthorizedEvent, onPaymentAuthorized: OnAuthorizedCallback): Promise<void> { + onpaymentauthorized( + event: ApplePayJS.ApplePayPaymentAuthorizedEvent, + onPaymentAuthorized: ApplePayServiceOptions['onPaymentAuthorized'] + ): Promise<void> { return new Promise((resolve, reject) => onPaymentAuthorized(resolve, reject, event)) - .then((result: ApplePayJS.ApplePayPaymentAuthorizationResult) => { - this.session.completePayment({ - ...result, - status: result?.status ?? ApplePaySession.STATUS_SUCCESS - }); + .then((result: ApplePayPaymentAuthorizationResult) => { + this.session.completePayment(result); }) - .catch((result?: ApplePayJS.ApplePayPaymentAuthorizationResult) => { - this.session.completePayment({ - ...result, - status: result?.status ?? ApplePaySession.STATUS_FAILURE - }); + .catch((result: ApplePayPaymentAuthorizationResult) => { + this.session.completePayment(result); }); } @@ -99,7 +103,6 @@ class ApplePayService { onpaymentmethodselected(event: ApplePayJS.ApplePayPaymentMethodSelectedEvent, onPaymentMethodSelected) { return new Promise((resolve, reject) => onPaymentMethodSelected(resolve, reject, event)) .then((paymentMethodUpdate: ApplePayJS.ApplePayPaymentMethodUpdate) => { - console.log('onpaymentmethodselected', paymentMethodUpdate); this.session.completePaymentMethodSelection(paymentMethodUpdate); }) .catch((paymentMethodUpdate: ApplePayJS.ApplePayPaymentMethodUpdate) => { diff --git a/packages/lib/src/components/ApplePay/types.ts b/packages/lib/src/components/ApplePay/types.ts index 7746bb7fa7..526c5fc654 100644 --- a/packages/lib/src/components/ApplePay/types.ts +++ b/packages/lib/src/components/ApplePay/types.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars import { UIElementProps } from '../internal/UIElement/types'; declare global { @@ -9,6 +8,18 @@ declare global { type Initiative = 'web' | 'messaging'; +export type ApplePayPaymentOrderDetails = { + orderTypeIdentifier: string; + orderIdentifier: string; + webServiceURL: string; + authenticationToken: string; +}; + +// @types/applepayjs package does not contain 'orderDetails' yet, so we create our own type +export type ApplePayPaymentAuthorizationResult = ApplePayJS.ApplePayPaymentAuthorizationResult & { + orderDetails?: ApplePayPaymentOrderDetails; +}; + export type ApplePayButtonType = | 'plain' | 'buy' @@ -25,12 +36,6 @@ export type ApplePayButtonType = | 'tip' | 'top-up'; -export type OnAuthorizedCallback = ( - resolve: (result?: ApplePayJS.ApplePayPaymentAuthorizationResult) => void, - reject: (result?: ApplePayJS.ApplePayPaymentAuthorizationResult) => void, - event: ApplePayJS.ApplePayPaymentAuthorizedEvent -) => void; - export interface ApplePayConfiguration extends UIElementProps { /** * The Apple Pay version number your website supports. @@ -143,6 +148,16 @@ export interface ApplePayConfiguration extends UIElementProps { onClick?: (resolve, reject) => void; + /** + * Collect the order tracking details if available. + * This callback is invoked when a successfull payment is resolved + * + * {@link https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentorderdetails} + * @param resolve - Must be called with the orderDetails fields + * @param reject - Must be called if something failed during the order creation + */ + onOrderTrackingRequest?: (resolve: (orderDetails: ApplePayPaymentOrderDetails) => void, reject: () => void) => void; + onValidateMerchant?: (resolve, reject, validationURL: string) => void; /** diff --git a/yarn.lock b/yarn.lock index 030939b98a..cbc372f86f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4350,10 +4350,10 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@types/applepayjs@3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/applepayjs/-/applepayjs-3.0.4.tgz#9806a4b3ccd73dcf169c61a34be7a39f91d77540" - integrity sha512-RqaVZWy1Kj4e1PoUoOI8uA+4UuuLpicQFxfU9Y/xWJFZFT6mFB4PiiY911iDxFk7pdvaj5HKH7VsWRisRca1Rg== +"@types/applepayjs@14.0.3": + version "14.0.3" + resolved "https://registry.yarnpkg.com/@types/applepayjs/-/applepayjs-14.0.3.tgz#3983c596385d8bd35379b5f5cb0287b46f723624" + integrity sha512-3ketgiX96+ZbFpK1/aCvEAEHUlPsuBt7cv1VCUVYZfWEekvn6oKaTtzK4G+CUINOQwh+1U5QwFb270xhOn94gA== "@types/aria-query@^5.0.1": version "5.0.1" @@ -4514,10 +4514,10 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/googlepay@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@types/googlepay/-/googlepay-0.7.0.tgz#eb11aca185a64cd413952bcb7c16ea25217ac6bc" - integrity sha512-jC7ViexJeV8LlTKLiUBfNs5GICbm0PYsm5Y30JCEBkreY0bMNA+F4KnTEz+WtqBTRldTAxKBKqRstlOUFpW+dA== +"@types/googlepay@0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@types/googlepay/-/googlepay-0.7.5.tgz#b944cd0e4c49f4661c9b966cb45614eb7ae87a26" + integrity sha512-158egcRaqkMSpW6unyGV4uG4FpoCklRf3J5emCzOXSRVAohMfIuZ481JNvp4X6+KxoNjxWiGtMx5vb1YfQADPw== "@types/graceful-fs@^4.1.3": version "4.1.9" @@ -13233,11 +13233,16 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9, regenerator-runtime@^0.14.0: +regenerator-runtime@^0.13.11: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" From a96dc2abfc82b7a07491d4f310a5edf6a2029add Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Mon, 11 Dec 2023 13:35:59 -0300 Subject: [PATCH 16/55] applepay: exposing authorization event --- .../lib/src/components/ApplePay/ApplePay.tsx | 4 +++- packages/lib/src/components/Dropin/Dropin.tsx | 24 +++++++++++++++---- .../src/components/GooglePay/GooglePay.tsx | 11 ++++++--- .../internal/UIElement/UIElement.tsx | 12 ++++++++-- .../wallets/ApplePayExpress.stories.tsx | 8 ++++++- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index c413d479b6..60351c7423 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -30,6 +30,7 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { this.startSession = this.startSession.bind(this); this.submit = this.submit.bind(this); this.validateMerchant = this.validateMerchant.bind(this); + this.collectOrderTrackingDetailsIfNeeded = this.collectOrderTrackingDetailsIfNeeded.bind(this); } /** @@ -96,7 +97,8 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { onValidateMerchant: onValidateMerchant || this.validateMerchant, onPaymentAuthorized: (resolve, reject, event) => { this.setState({ - applePayToken: btoa(JSON.stringify(event.payment.token.paymentData)) + applePayToken: btoa(JSON.stringify(event.payment.token.paymentData)), + authorizedEvent: event }); this.makePaymentsCall() diff --git a/packages/lib/src/components/Dropin/Dropin.tsx b/packages/lib/src/components/Dropin/Dropin.tsx index 77f5097d5a..286bd72053 100644 --- a/packages/lib/src/components/Dropin/Dropin.tsx +++ b/packages/lib/src/components/Dropin/Dropin.tsx @@ -47,8 +47,16 @@ class DropinElement extends UIElement<DropinConfiguration> { }; } + public override get authorizedEvent(): any { + return this.dropinRef?.state?.activePaymentMethod?.authorizedEvent; + } + get isValid() { - return !!this.dropinRef && !!this.dropinRef.state.activePaymentMethod && !!this.dropinRef.state.activePaymentMethod.isValid; + return ( + !!this.dropinRef && + !!this.dropinRef.state.activePaymentMethod && + !!this.dropinRef.state.activePaymentMethod.isValid + ); } showValidation() { @@ -103,7 +111,8 @@ class DropinElement extends UIElement<DropinConfiguration> { * Creates the Drop-in elements */ private handleCreate = () => { - const { paymentMethodsConfiguration, showStoredPaymentMethods, showPaymentMethods, instantPaymentTypes } = this.props; + const { paymentMethodsConfiguration, showStoredPaymentMethods, showPaymentMethods, instantPaymentTypes } = + this.props; const { paymentMethods, storedPaymentMethods, instantPaymentMethods } = splitPaymentMethods( this.core.paymentMethodsResponse, @@ -115,8 +124,15 @@ class DropinElement extends UIElement<DropinConfiguration> { const storedElements = showStoredPaymentMethods ? createStoredElements(storedPaymentMethods, paymentMethodsConfiguration, commonProps, this.core) : []; - const elements = showPaymentMethods ? createElements(paymentMethods, paymentMethodsConfiguration, commonProps, this.core) : []; - const instantPaymentElements = createInstantPaymentElements(instantPaymentMethods, paymentMethodsConfiguration, commonProps, this.core); + const elements = showPaymentMethods + ? createElements(paymentMethods, paymentMethodsConfiguration, commonProps, this.core) + : []; + const instantPaymentElements = createInstantPaymentElements( + instantPaymentMethods, + paymentMethodsConfiguration, + commonProps, + this.core + ); return [storedElements, elements, instantPaymentElements]; }; diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index b520ee850e..f157dcd2b8 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -39,7 +39,10 @@ class GooglePay extends UIElement<GooglePayConfiguration> { const buttonSizeMode = props.buttonSizeMode ?? (props.isDropin ? 'fill' : 'static'); const buttonLocale = getGooglePayLocale(props.buttonLocale ?? props.i18n?.locale); - const callbackIntents: google.payments.api.CallbackIntent[] = [...props.callbackIntents, 'PAYMENT_AUTHORIZATION']; + const callbackIntents: google.payments.api.CallbackIntent[] = [ + ...props.callbackIntents, + 'PAYMENT_AUTHORIZATION' + ]; return { ...props, @@ -81,9 +84,11 @@ class GooglePay extends UIElement<GooglePayConfiguration> { * * @see https://developers.google.com/pay/api/web/reference/client#onPaymentAuthorized **/ - private onPaymentAuthorized = async (paymentData: google.payments.api.PaymentData): Promise<google.payments.api.PaymentAuthorizationResult> => { + private onPaymentAuthorized = async ( + paymentData: google.payments.api.PaymentData + ): Promise<google.payments.api.PaymentAuthorizationResult> => { this.setState({ - authorizedData: paymentData, + authorizedEvent: paymentData, googlePayToken: paymentData.paymentMethodData.tokenizationData.token, googlePayCardNetwork: paymentData.paymentMethodData.info.cardNetwork }); diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 0816ca7d83..c4117c9300 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -92,6 +92,15 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> return Promise.resolve(); } + // ApplePayJS.ApplePayPayment | google.payments.api.PaymentData | undefined + /** + * Certain payment methods have data returned after the shopper authorization step (Ex: GooglePay, ApplePay) + * This getter returns the event data in case it is available + */ + public get authorizedEvent(): any { + return this.state.authorizedEvent; + } + public setState(newState: object): void { this.state = { ...this.state, ...newState }; this.onChange(); @@ -164,8 +173,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> this.props.onSubmit( { data: this.data, - isValid: this.isValid, - ...(this.state.authorizedData && { authorizedData: this.state.authorizedData }) + isValid: this.isValid }, this.elementRef, { resolve, reject } diff --git a/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx b/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx index 0c13445d89..5261405de2 100644 --- a/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx +++ b/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx @@ -171,7 +171,13 @@ export const Express: ApplePayStory = { if (countryCode === 'BR') { update = { newTotal: ApplePayAmountHelper.getApplePayTotal(), - errors: [new ApplePayError('shippingContactInvalid', 'countryCode', 'Cannot ship to the selected address')] + errors: [ + new ApplePayError( + 'shippingContactInvalid', + 'countryCode', + 'Cannot ship to the selected address' + ) + ] }; resolve(update); return; From 077870fcacd46bec56a083d0d4517c4778b5f239 Mon Sep 17 00:00:00 2001 From: guilhermer <> Date: Tue, 12 Dec 2023 10:31:51 -0300 Subject: [PATCH 17/55] untested applepay address format --- .../lib/src/components/ApplePay/ApplePay.tsx | 15 ++++-- packages/lib/src/components/ApplePay/types.ts | 4 ++ packages/lib/src/components/ApplePay/utils.ts | 47 +++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index 60351c7423..e24353c8a6 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -7,7 +7,7 @@ import defaultProps from './defaultProps'; import { httpPost } from '../../core/Services/http'; import { APPLEPAY_SESSION_ENDPOINT } from './config'; import { preparePaymentRequest } from './payment-request'; -import { resolveSupportedVersion, mapBrands } from './utils'; +import { resolveSupportedVersion, mapBrands, formatApplePayContactToAdyenAddressFormat } from './utils'; import { ApplePayConfiguration, ApplePayElementData, @@ -53,10 +53,14 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { * Formats the component data output */ protected formatData(): ApplePayElementData { + const { applePayToken, billingAddress, shippingAddress } = this.state; + return { paymentMethod: { type: ApplePayElement.type, - applePayToken: this.state.applePayToken + applePayToken, + ...(billingAddress && { billingAddress }), + ...(shippingAddress && { shippingAddress }) } }; } @@ -96,9 +100,14 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { onShippingContactSelected, onValidateMerchant: onValidateMerchant || this.validateMerchant, onPaymentAuthorized: (resolve, reject, event) => { + const billingAddress = formatApplePayContactToAdyenAddressFormat(event.payment.billingContact); + const shippingAddress = formatApplePayContactToAdyenAddressFormat(event.payment.shippingContact); + this.setState({ applePayToken: btoa(JSON.stringify(event.payment.token.paymentData)), - authorizedEvent: event + authorizedEvent: event, + ...(billingAddress && { billingAddress }), + ...(shippingAddress && { shippingAddress }) }); this.makePaymentsCall() diff --git a/packages/lib/src/components/ApplePay/types.ts b/packages/lib/src/components/ApplePay/types.ts index 526c5fc654..212f1df0b1 100644 --- a/packages/lib/src/components/ApplePay/types.ts +++ b/packages/lib/src/components/ApplePay/types.ts @@ -1,4 +1,6 @@ import { UIElementProps } from '../internal/UIElement/types'; +import { AddressSchema } from '../internal/Address/types'; +import { AddressData } from '../../types/global-types'; declare global { interface Window { @@ -198,6 +200,8 @@ export interface ApplePayElementData { paymentMethod: { type: string; applePayToken: string; + billingAddress?: AddressData; + shippingAddress?: AddressData; }; } diff --git a/packages/lib/src/components/ApplePay/utils.ts b/packages/lib/src/components/ApplePay/utils.ts index 14d0fbc7ee..db3bdfbd54 100644 --- a/packages/lib/src/components/ApplePay/utils.ts +++ b/packages/lib/src/components/ApplePay/utils.ts @@ -1,3 +1,5 @@ +import { AddressData } from '../../types/global-types'; + export function resolveSupportedVersion(latestVersion: number): number | null { const versions = []; for (let i = latestVersion; i > 0; i--) { @@ -36,3 +38,48 @@ export function mapBrands(brands) { return accumulator; }, []); } + +/** + * ApplePay formats address into two lines (US format). First line includes house number and street name. + * Second line includes unit/suite/apartment number if applicable. + * This function formats it into Adyen's Address format (house number separate from street). + */ +export function formatApplePayContactToAdyenAddressFormat( + paymentContact: ApplePayJS.ApplePayPaymentContact +): AddressData | undefined { + if (!paymentContact) { + return; + } + + let street = ''; + let houseNumberOrName = ''; + if (paymentContact.addressLines && paymentContact.addressLines.length) { + const splitAddress = splitAddressLine(paymentContact.addressLines[0]); + street = splitAddress.streetAddress; + houseNumberOrName = splitAddress.houseNumber; + } + + if (paymentContact.addressLines && paymentContact.addressLines.length > 1) { + street += ` ${paymentContact.addressLines[1]}`; + } + + return { + city: paymentContact.locality, + country: paymentContact.countryCode, + houseNumberOrName, + postalCode: paymentContact.postalCode, + stateOrProvince: paymentContact.administrativeArea, + street: street + }; +} + +const splitAddressLine = (addressLine: string) => { + // The \d+ captures the digits of the house number, and \w* allows for any letter suffixes (like "123B") + // Everything after the space is considered the street address. + const parts = addressLine.match(/^(\d+\w*)\s+(.+)/); + if (parts) { + return { houseNumber: parts[1] || '', streetAddress: parts[2] || addressLine }; + } else { + return { houseNumber: '', streetAddress: addressLine }; + } +}; From 582001668afb4ec9b85e769d892cd588adc566d2 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 14 Dec 2023 15:31:55 -0300 Subject: [PATCH 18/55] applepay - formatting data accordingly --- .../lib/src/components/ApplePay/ApplePay.tsx | 14 ++--- packages/lib/src/components/ApplePay/types.ts | 1 - .../lib/src/components/ApplePay/utils.test.ts | 58 +++++++++++++++++++ packages/lib/src/components/ApplePay/utils.ts | 43 +++++--------- 4 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 packages/lib/src/components/ApplePay/utils.test.ts diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index e24353c8a6..50c46a5d62 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -53,15 +53,15 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { * Formats the component data output */ protected formatData(): ApplePayElementData { - const { applePayToken, billingAddress, shippingAddress } = this.state; + const { applePayToken, billingAddress, deliveryAddress } = this.state; return { paymentMethod: { type: ApplePayElement.type, - applePayToken, - ...(billingAddress && { billingAddress }), - ...(shippingAddress && { shippingAddress }) - } + applePayToken + }, + ...(billingAddress && { billingAddress }), + ...(deliveryAddress && { deliveryAddress }) }; } @@ -101,13 +101,13 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { onValidateMerchant: onValidateMerchant || this.validateMerchant, onPaymentAuthorized: (resolve, reject, event) => { const billingAddress = formatApplePayContactToAdyenAddressFormat(event.payment.billingContact); - const shippingAddress = formatApplePayContactToAdyenAddressFormat(event.payment.shippingContact); + const deliveryAddress = formatApplePayContactToAdyenAddressFormat(event.payment.shippingContact, true); this.setState({ applePayToken: btoa(JSON.stringify(event.payment.token.paymentData)), authorizedEvent: event, ...(billingAddress && { billingAddress }), - ...(shippingAddress && { shippingAddress }) + ...(deliveryAddress && { deliveryAddress }) }); this.makePaymentsCall() diff --git a/packages/lib/src/components/ApplePay/types.ts b/packages/lib/src/components/ApplePay/types.ts index 212f1df0b1..15a4e3178b 100644 --- a/packages/lib/src/components/ApplePay/types.ts +++ b/packages/lib/src/components/ApplePay/types.ts @@ -1,5 +1,4 @@ import { UIElementProps } from '../internal/UIElement/types'; -import { AddressSchema } from '../internal/Address/types'; import { AddressData } from '../../types/global-types'; declare global { diff --git a/packages/lib/src/components/ApplePay/utils.test.ts b/packages/lib/src/components/ApplePay/utils.test.ts new file mode 100644 index 0000000000..a59b9a9d48 --- /dev/null +++ b/packages/lib/src/components/ApplePay/utils.test.ts @@ -0,0 +1,58 @@ +import { formatApplePayContactToAdyenAddressFormat } from './utils'; + +describe('formatApplePayContactToAdyenAddressFormat()', () => { + test('should build the street by merging the address lines and set houseNumberOrName to ZZ', () => { + const billingContact = { + addressLines: ['1 Infinite Loop', 'Unit 100'], + locality: 'Cupertino', + administrativeArea: 'CA', + postalCode: '95014', + countryCode: 'US', + country: 'United States', + givenName: 'John', + familyName: 'Appleseed', + phoneticFamilyName: '', + phoneticGivenName: '', + subAdministrativeArea: '', + subLocality: '' + }; + + const billingAddress = formatApplePayContactToAdyenAddressFormat(billingContact); + + expect(billingAddress.houseNumberOrName).toEqual('ZZ'); + expect(billingAddress.street).toEqual('1 Infinite Loop Unit 100'); + expect(billingAddress.city).toEqual('Cupertino'); + expect(billingAddress.postalCode).toEqual('95014'); + expect(billingAddress.country).toEqual('US'); + expect(billingAddress.stateOrProvince).toEqual('CA'); + }); + test('should return only postal code if available', () => { + const billingContact = { + addressLines: [], + locality: '', + administrativeArea: '', + postalCode: '95014', + countryCode: '', + country: '', + givenName: '', + familyName: '', + phoneticFamilyName: '', + phoneticGivenName: '', + subAdministrativeArea: '', + subLocality: '' + }; + + const billingAddress = formatApplePayContactToAdyenAddressFormat(billingContact); + + expect(billingAddress.houseNumberOrName).toEqual(''); + expect(billingAddress.street).toEqual(''); + expect(billingAddress.city).toEqual(''); + expect(billingAddress.postalCode).toEqual('95014'); + expect(billingAddress.country).toEqual(''); + expect(billingAddress.stateOrProvince).toEqual(''); + }); + + test.todo('should return firstName and lastName if the contact is for delivery address'); + + test.todo('should omit stateOrProvince field if not available'); +}); diff --git a/packages/lib/src/components/ApplePay/utils.ts b/packages/lib/src/components/ApplePay/utils.ts index db3bdfbd54..71208aa9a0 100644 --- a/packages/lib/src/components/ApplePay/utils.ts +++ b/packages/lib/src/components/ApplePay/utils.ts @@ -40,46 +40,29 @@ export function mapBrands(brands) { } /** - * ApplePay formats address into two lines (US format). First line includes house number and street name. - * Second line includes unit/suite/apartment number if applicable. - * This function formats it into Adyen's Address format (house number separate from street). + * This function formats Apple Pay contact format to Adyen address format + * + * Setting 'houseNumberOrName' to ZZ won't affect the AVS check, and it will make the algorithm take the + * house number from the 'street' property. */ export function formatApplePayContactToAdyenAddressFormat( - paymentContact: ApplePayJS.ApplePayPaymentContact + paymentContact: ApplePayJS.ApplePayPaymentContact, + isDeliveryAddress?: boolean ): AddressData | undefined { if (!paymentContact) { return; } - let street = ''; - let houseNumberOrName = ''; - if (paymentContact.addressLines && paymentContact.addressLines.length) { - const splitAddress = splitAddressLine(paymentContact.addressLines[0]); - street = splitAddress.streetAddress; - houseNumberOrName = splitAddress.houseNumber; - } - - if (paymentContact.addressLines && paymentContact.addressLines.length > 1) { - street += ` ${paymentContact.addressLines[1]}`; - } - return { city: paymentContact.locality, country: paymentContact.countryCode, - houseNumberOrName, + houseNumberOrName: 'ZZ', postalCode: paymentContact.postalCode, - stateOrProvince: paymentContact.administrativeArea, - street: street + street: paymentContact.addressLines.join(' ').trim(), + ...(paymentContact.administrativeArea && { stateOrProvince: paymentContact.administrativeArea }), + ...(isDeliveryAddress && { + firstName: paymentContact.givenName, + lastName: paymentContact.familyName + }) }; } - -const splitAddressLine = (addressLine: string) => { - // The \d+ captures the digits of the house number, and \w* allows for any letter suffixes (like "123B") - // Everything after the space is considered the street address. - const parts = addressLine.match(/^(\d+\w*)\s+(.+)/); - if (parts) { - return { houseNumber: parts[1] || '', streetAddress: parts[2] || addressLine }; - } else { - return { houseNumber: '', streetAddress: addressLine }; - } -}; From 1d0d7bb28acfe32b0b58b74fea45d438574dc81b Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Fri, 15 Dec 2023 09:28:11 -0300 Subject: [PATCH 19/55] applepay address formatter unit tests --- .../lib/src/components/ApplePay/utils.test.ts | 60 ++++++++++++++----- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/lib/src/components/ApplePay/utils.test.ts b/packages/lib/src/components/ApplePay/utils.test.ts index a59b9a9d48..c58dc90f80 100644 --- a/packages/lib/src/components/ApplePay/utils.test.ts +++ b/packages/lib/src/components/ApplePay/utils.test.ts @@ -19,15 +19,18 @@ describe('formatApplePayContactToAdyenAddressFormat()', () => { const billingAddress = formatApplePayContactToAdyenAddressFormat(billingContact); - expect(billingAddress.houseNumberOrName).toEqual('ZZ'); - expect(billingAddress.street).toEqual('1 Infinite Loop Unit 100'); - expect(billingAddress.city).toEqual('Cupertino'); - expect(billingAddress.postalCode).toEqual('95014'); - expect(billingAddress.country).toEqual('US'); - expect(billingAddress.stateOrProvince).toEqual('CA'); + expect(billingAddress).toStrictEqual({ + postalCode: '95014', + houseNumberOrName: 'ZZ', + street: '1 Infinite Loop Unit 100', + city: 'Cupertino', + country: 'US', + stateOrProvince: 'CA' + }); }); + test('should return only postal code if available', () => { - const billingContact = { + const billingContact: ApplePayJS.ApplePayPaymentContact = { addressLines: [], locality: '', administrativeArea: '', @@ -44,15 +47,42 @@ describe('formatApplePayContactToAdyenAddressFormat()', () => { const billingAddress = formatApplePayContactToAdyenAddressFormat(billingContact); - expect(billingAddress.houseNumberOrName).toEqual(''); - expect(billingAddress.street).toEqual(''); - expect(billingAddress.city).toEqual(''); - expect(billingAddress.postalCode).toEqual('95014'); - expect(billingAddress.country).toEqual(''); - expect(billingAddress.stateOrProvince).toEqual(''); + expect(billingAddress).toStrictEqual({ + postalCode: '95014', + houseNumberOrName: 'ZZ', + street: '', + city: '', + country: '' + }); }); - test.todo('should return firstName and lastName if the contact is for delivery address'); + test('should return firstName and lastName if the contact is for delivery address', () => { + const deliveryContact: ApplePayJS.ApplePayPaymentContact = { + addressLines: ['802 Richardon Drive', 'Brooklyn'], + locality: 'New York', + administrativeArea: 'NY', + postalCode: '11213', + countryCode: 'US', + country: 'United States', + givenName: 'Jonny', + familyName: 'Smithson', + phoneticFamilyName: '', + phoneticGivenName: '', + subAdministrativeArea: '', + subLocality: '' + }; + + const deliveryAddress = formatApplePayContactToAdyenAddressFormat(deliveryContact, true); - test.todo('should omit stateOrProvince field if not available'); + expect(deliveryAddress).toStrictEqual({ + street: '802 Richardon Drive Brooklyn', + houseNumberOrName: 'ZZ', + city: 'New York', + postalCode: '11213', + country: 'US', + stateOrProvince: 'NY', + firstName: 'Jonny', + lastName: 'Smithson' + }); + }); }); From 324a3ae0ae4afd34dc6a53baae92e1cd24e9302f Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Fri, 15 Dec 2023 12:08:50 -0300 Subject: [PATCH 20/55] feat: parsing googlepay address --- .../src/components/GooglePay/GooglePay.tsx | 27 +++++-- .../src/components/GooglePay/utils.test.ts | 74 +++++++++++++++++++ .../lib/src/components/GooglePay/utils.ts | 29 ++++++++ 3 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 packages/lib/src/components/GooglePay/utils.test.ts diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index f157dcd2b8..3f47d2a566 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -4,12 +4,12 @@ import GooglePayService from './GooglePayService'; import GooglePayButton from './components/GooglePayButton'; import defaultProps from './defaultProps'; import { GooglePayConfiguration } from './types'; -import { getGooglePayLocale } from './utils'; +import { formatGooglePayContactToAdyenAddressFormat, getGooglePayLocale } from './utils'; import collectBrowserInfo from '../../utils/browserInfo'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; import { onSubmitReject } from '../../core/types'; -import { PaymentResponseData } from '../../types/global-types'; +import { AddressData, PaymentResponseData } from '../../types/global-types'; class GooglePay extends UIElement<GooglePayConfiguration> { public static type = TxVariants.googlepay; @@ -57,13 +57,18 @@ class GooglePay extends UIElement<GooglePayConfiguration> { * Formats the component data output */ formatData() { + const { googlePayCardNetwork, googlePayToken, billingAddress, deliveryAddress } = this.state; + return { paymentMethod: { type: this.type, - googlePayCardNetwork: this.state.googlePayCardNetwork, - googlePayToken: this.state.googlePayToken + googlePayCardNetwork, + googlePayToken }, - browserInfo: this.browserInfo + browserInfo: this.browserInfo, + origin: !!window && window.location.origin, + ...(billingAddress && { billingAddress }), + ...(deliveryAddress && { deliveryAddress }) }; } @@ -87,10 +92,20 @@ class GooglePay extends UIElement<GooglePayConfiguration> { private onPaymentAuthorized = async ( paymentData: google.payments.api.PaymentData ): Promise<google.payments.api.PaymentAuthorizationResult> => { + const billingAddress: AddressData = formatGooglePayContactToAdyenAddressFormat( + paymentData.paymentMethodData.info.billingAddress + ); + const deliveryAddress: AddressData = formatGooglePayContactToAdyenAddressFormat( + paymentData.shippingAddress, + true + ); + this.setState({ authorizedEvent: paymentData, googlePayToken: paymentData.paymentMethodData.tokenizationData.token, - googlePayCardNetwork: paymentData.paymentMethodData.info.cardNetwork + googlePayCardNetwork: paymentData.paymentMethodData.info.cardNetwork, + ...(billingAddress && { billingAddress }), + ...(deliveryAddress && { deliveryAddress }) }); return new Promise<google.payments.api.PaymentAuthorizationResult>(resolve => { diff --git a/packages/lib/src/components/GooglePay/utils.test.ts b/packages/lib/src/components/GooglePay/utils.test.ts new file mode 100644 index 0000000000..f14685ca92 --- /dev/null +++ b/packages/lib/src/components/GooglePay/utils.test.ts @@ -0,0 +1,74 @@ +import { formatGooglePayContactToAdyenAddressFormat } from './utils'; + +describe('formatGooglePayContactToAdyenAddressFormat()', () => { + test('should build the street by merging the addresses and set houseNumberOrName to ZZ', () => { + const billingContact: google.payments.api.Address = { + phoneNumber: '+1 650-555-5555', + address1: '1600 Amphitheatre Parkway', + address2: 'Brooklyn', + address3: '', + sortingCode: '', + countryCode: 'US', + postalCode: '94043', + name: 'Card Holder Name', + locality: 'Mountain View', + administrativeArea: 'CA' + }; + + const billingAddress = formatGooglePayContactToAdyenAddressFormat(billingContact); + + expect(billingAddress).toStrictEqual({ + postalCode: '94043', + houseNumberOrName: 'ZZ', + street: '1600 Amphitheatre Parkway Brooklyn', + city: 'Mountain View', + country: 'US', + stateOrProvince: 'CA' + }); + }); + + test('should return postal code and available fields', () => { + const billingContact: Partial<google.payments.api.Address> = { + countryCode: 'US', + postalCode: '94043', + name: 'Card Holder Name' + }; + + const billingAddress = formatGooglePayContactToAdyenAddressFormat(billingContact); + + expect(billingAddress).toStrictEqual({ + postalCode: '94043', + houseNumberOrName: 'ZZ', + country: 'US', + street: '', + city: '' + }); + }); + + test('should return firstName and lastName if the contact is for delivery address', () => { + const deliveryContact: Partial<google.payments.api.Address> = { + phoneNumber: '+61 2 9374 4000', + address1: '48 Pirrama Road', + address2: '', + address3: '', + sortingCode: '', + countryCode: 'AU', + postalCode: '2009', + name: 'Australian User', + locality: 'Sydney', + administrativeArea: 'NSW' + }; + + const deliveryAddress = formatGooglePayContactToAdyenAddressFormat(deliveryContact, true); + + expect(deliveryAddress).toStrictEqual({ + postalCode: '2009', + country: 'AU', + street: '48 Pirrama Road', + houseNumberOrName: 'ZZ', + city: 'Sydney', + stateOrProvince: 'NSW', + firstName: 'Australian User' + }); + }); +}); diff --git a/packages/lib/src/components/GooglePay/utils.ts b/packages/lib/src/components/GooglePay/utils.ts index 737a75e748..8c767d96dd 100644 --- a/packages/lib/src/components/GooglePay/utils.ts +++ b/packages/lib/src/components/GooglePay/utils.ts @@ -1,3 +1,5 @@ +import { AddressData } from '../../types/global-types'; + /** * */ @@ -16,6 +18,33 @@ export function resolveEnvironment(env = 'TEST'): google.payments.api.Environmen } } +/** + * This function formats Google Pay contact format to Adyen address format + * + * Setting 'houseNumberOrName' to ZZ won't affect the AVS check, and it will make the algorithm take the + * house number from the 'street' property. + */ +export function formatGooglePayContactToAdyenAddressFormat( + paymentContact?: Partial<google.payments.api.Address>, + isDeliveryAddress?: boolean +): AddressData | undefined { + if (!paymentContact) { + return; + } + + return { + postalCode: paymentContact.postalCode, + country: paymentContact.countryCode, + street: [paymentContact.address1, paymentContact.address2, paymentContact.address3].join(' ').trim(), + houseNumberOrName: 'ZZ', + city: paymentContact.locality || '', + ...(paymentContact.administrativeArea && { stateOrProvince: paymentContact.administrativeArea }), + ...(isDeliveryAddress && { + firstName: paymentContact.name + }) + }; +} + // export function mapBrands(brands) { // const brandMapping = { // mc: 'MASTERCARD', From 0c7c16dc1fb82d239c4764e269ba6b26791e911a Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Mon, 18 Dec 2023 12:41:44 -0300 Subject: [PATCH 21/55] using onAuthorized on gpay,applepay,paypal --- .../lib/src/components/ApplePay/ApplePay.tsx | 31 +++++++- .../src/components/ApplePay/defaultProps.ts | 1 - packages/lib/src/components/ApplePay/types.ts | 18 ++++- packages/lib/src/components/Dropin/Dropin.tsx | 4 - .../src/components/GooglePay/GooglePay.tsx | 33 ++++++-- .../src/components/GooglePay/defaultProps.ts | 1 - .../lib/src/components/GooglePay/types.ts | 18 ++++- packages/lib/src/components/PayPal/Paypal.tsx | 44 ++++++++--- .../lib/src/components/PayPal/defaultProps.ts | 1 - packages/lib/src/components/PayPal/types.ts | 18 +++-- .../utils/create-shopper-details.test.ts | 79 ------------------- .../PayPal/utils/create-shopper-details.ts | 63 --------------- ...at-paypal-order-contact-to-adyen-format.ts | 33 ++++++++ .../internal/UIElement/UIElement.tsx | 9 --- packages/lib/src/types/global-types.ts | 6 +- .../playground/src/pages/Dropin/manual.js | 59 ++++++++------ .../playground/src/pages/Dropin/session.js | 9 ++- .../playground/src/pages/Wallets/Wallets.js | 19 +++-- 18 files changed, 226 insertions(+), 220 deletions(-) delete mode 100644 packages/lib/src/components/PayPal/utils/create-shopper-details.test.ts delete mode 100644 packages/lib/src/components/PayPal/utils/create-shopper-details.ts create mode 100644 packages/lib/src/components/PayPal/utils/format-paypal-order-contact-to-adyen-format.ts diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index 50c46a5d62..64195141f2 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -31,6 +31,7 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { this.submit = this.submit.bind(this); this.validateMerchant = this.validateMerchant.bind(this); this.collectOrderTrackingDetailsIfNeeded = this.collectOrderTrackingDetailsIfNeeded.bind(this); + this.handleAuthorization = this.handleAuthorization.bind(this); } /** @@ -110,7 +111,8 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { ...(deliveryAddress && { deliveryAddress }) }); - this.makePaymentsCall() + this.handleAuthorization() + .then(this.makePaymentsCall) .then(this.sanitizeResponse) .then(this.verifyPaymentDidNotFail) .then(this.collectOrderTrackingDetailsIfNeeded) @@ -125,6 +127,8 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { this.handleResponse(paymentResponse); }) .catch((error: onSubmitReject) => { + console.error(error); + this.setElementStatus('ready'); const errors = error?.error?.applePayError; @@ -145,6 +149,31 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { })); } + /** + * Call the 'onAuthorized' callback if available. + * Must be resolved/reject for the payment flow to continue + * + * @private + */ + private async handleAuthorization(): Promise<void> { + return new Promise<void>((resolve, reject) => { + if (!this.props.onAuthorized) { + resolve(); + } + + const { authorizedEvent, billingAddress, deliveryAddress } = this.state; + + this.props.onAuthorized( + { + authorizedEvent, + ...(billingAddress && { billingAddress }), + ...(deliveryAddress && { deliveryAddress }) + }, + { resolve, reject } + ); + }); + } + /** * Verify if the 'onOrderTrackingRequest' is provided. If so, triggers the callback expecting an * Apple Pay order details back diff --git a/packages/lib/src/components/ApplePay/defaultProps.ts b/packages/lib/src/components/ApplePay/defaultProps.ts index 23f59463cc..4411073d07 100644 --- a/packages/lib/src/components/ApplePay/defaultProps.ts +++ b/packages/lib/src/components/ApplePay/defaultProps.ts @@ -76,7 +76,6 @@ const defaultProps = { // Events onClick: resolve => resolve(), - onAuthorized: resolve => resolve(), onPaymentMethodSelected: null, onShippingContactSelected: null, onShippingMethodSelected: null, diff --git a/packages/lib/src/components/ApplePay/types.ts b/packages/lib/src/components/ApplePay/types.ts index 15a4e3178b..7ce2bfdd4a 100644 --- a/packages/lib/src/components/ApplePay/types.ts +++ b/packages/lib/src/components/ApplePay/types.ts @@ -149,13 +149,29 @@ export interface ApplePayConfiguration extends UIElementProps { onClick?: (resolve, reject) => void; + /** + * Callback called when ApplePay authorize the payment. + * Must be resolved/rejected with the action object. + * + * @param paymentData + * @returns + */ + onAuthorized?: ( + data: { + authorizedEvent: ApplePayJS.ApplePayPaymentAuthorizedEvent; + billingAddress?: Partial<AddressData>; + deliveryAddress?: Partial<AddressData>; + }, + actions: { resolve: () => void; reject: () => void } + ) => void; + /** * Collect the order tracking details if available. * This callback is invoked when a successfull payment is resolved * * {@link https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentorderdetails} * @param resolve - Must be called with the orderDetails fields - * @param reject - Must be called if something failed during the order creation + * @param reject - Must be called if something failed during the order creation. Calling 'reject' won't cancel the payment flow */ onOrderTrackingRequest?: (resolve: (orderDetails: ApplePayPaymentOrderDetails) => void, reject: () => void) => void; diff --git a/packages/lib/src/components/Dropin/Dropin.tsx b/packages/lib/src/components/Dropin/Dropin.tsx index 286bd72053..91d318cd6a 100644 --- a/packages/lib/src/components/Dropin/Dropin.tsx +++ b/packages/lib/src/components/Dropin/Dropin.tsx @@ -47,10 +47,6 @@ class DropinElement extends UIElement<DropinConfiguration> { }; } - public override get authorizedEvent(): any { - return this.dropinRef?.state?.activePaymentMethod?.authorizedEvent; - } - get isValid() { return ( !!this.dropinRef && diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 3f47d2a566..a6fe87b032 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -20,6 +20,7 @@ class GooglePay extends UIElement<GooglePayConfiguration> { constructor(props) { super(props); + this.handleAuthorization = this.handleAuthorization.bind(this); this.googlePay = new GooglePayService({ ...this.props, @@ -30,10 +31,6 @@ class GooglePay extends UIElement<GooglePayConfiguration> { }); } - /** - * Formats the component data input - * For legacy support - maps configuration.merchantIdentifier to configuration.merchantId - */ formatProps(props): GooglePayConfiguration { // const allowedCardNetworks = props.brands?.length ? mapBrands(props.brands) : props.allowedCardNetworks; const buttonSizeMode = props.buttonSizeMode ?? (props.isDropin ? 'fill' : 'static'); @@ -109,7 +106,8 @@ class GooglePay extends UIElement<GooglePayConfiguration> { }); return new Promise<google.payments.api.PaymentAuthorizationResult>(resolve => { - this.makePaymentsCall() + this.handleAuthorization() + .then(this.makePaymentsCall) .then(this.sanitizeResponse) .then(this.verifyPaymentDidNotFail) .then((paymentResponse: PaymentResponseData) => { @@ -126,7 +124,7 @@ class GooglePay extends UIElement<GooglePayConfiguration> { transactionState: 'ERROR', error: { intent: error?.error?.googlePayError?.intent || 'PAYMENT_AUTHORIZATION', - message: error?.error?.googlePayError?.message || 'Something went wrong', + message: error?.error?.googlePayError?.message || 'Payment failed', reason: error?.error?.googlePayError?.reason || 'OTHER_ERROR' } }); @@ -134,6 +132,29 @@ class GooglePay extends UIElement<GooglePayConfiguration> { }); }; + /** + * Call the 'onAuthorized' callback if available. + * Must be resolved/reject for the payment flow to continue + */ + private async handleAuthorization(): Promise<void> { + return new Promise<void>((resolve, reject) => { + if (!this.props.onAuthorized) { + resolve(); + } + + const { authorizedEvent, billingAddress, deliveryAddress } = this.state; + + this.props.onAuthorized( + { + authorizedEvent, + ...(billingAddress && { billingAddress }), + ...(deliveryAddress && { deliveryAddress }) + }, + { resolve, reject } + ); + }); + } + /** * Validation */ diff --git a/packages/lib/src/components/GooglePay/defaultProps.ts b/packages/lib/src/components/GooglePay/defaultProps.ts index cf4e646aaa..0c62139100 100644 --- a/packages/lib/src/components/GooglePay/defaultProps.ts +++ b/packages/lib/src/components/GooglePay/defaultProps.ts @@ -30,7 +30,6 @@ export default { totalPriceStatus: 'FINAL' as google.payments.api.TotalPriceStatus, // Callbacks - onAuthorized: params => params, onClick: resolve => resolve(), // CardParameters diff --git a/packages/lib/src/components/GooglePay/types.ts b/packages/lib/src/components/GooglePay/types.ts index 088910e7a0..ada06be4c4 100644 --- a/packages/lib/src/components/GooglePay/types.ts +++ b/packages/lib/src/components/GooglePay/types.ts @@ -1,3 +1,4 @@ +import { AddressData } from '../../types'; import { UIElementProps } from '../internal/UIElement/types'; export interface GooglePayPropsConfiguration { @@ -149,7 +150,22 @@ export interface GooglePayConfiguration extends UIElementProps { // Events onClick?: (resolve, reject) => void; - onAuthorized?: (paymentData: google.payments.api.PaymentData) => void; + + /** + * Callback called when GooglePay authorize the payment. + * Must be resolved/rejected with the action object. + * + * @param paymentData + * @returns + */ + onAuthorized?: ( + data: { + authorizedEvent: google.payments.api.PaymentData; + billingAddress?: Partial<AddressData>; + deliveryAddress?: Partial<AddressData>; + }, + actions: { resolve: () => void; reject: () => void } + ) => void; } // Used to add undocumented google payment options diff --git a/packages/lib/src/components/PayPal/Paypal.tsx b/packages/lib/src/components/PayPal/Paypal.tsx index 55517faf0f..cff4b34629 100644 --- a/packages/lib/src/components/PayPal/Paypal.tsx +++ b/packages/lib/src/components/PayPal/Paypal.tsx @@ -8,8 +8,8 @@ import './Paypal.scss'; import CoreProvider from '../../core/Context/CoreProvider'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { ERRORS } from './constants'; -import { createShopperDetails } from './utils/create-shopper-details'; import { TxVariants } from '../tx-variants'; +import { formatPaypalOrderContatcToAdyenFormat } from './utils/format-paypal-order-contact-to-adyen-format'; class PaypalElement extends UIElement<PayPalConfiguration> { public static type = TxVariants.paypal; @@ -90,27 +90,47 @@ class PaypalElement extends UIElement<PayPalConfiguration> { return true; } - private handleCancel = () => { - this.handleError(new AdyenCheckoutError('CANCEL')); - }; - private handleOnApprove = (data: any, actions: any): Promise<void> | void => { - const { onShopperDetails } = this.props; + const { onAuthorized } = this.props; const state = { data: { details: data, paymentData: this.paymentData } }; - if (!onShopperDetails) { + if (!onAuthorized) { this.handleAdditionalDetails(state); return; } return actions.order .get() - .then(paypalOrder => { - const shopperDetails = createShopperDetails(paypalOrder); - return new Promise<void>((resolve, reject) => onShopperDetails(shopperDetails, paypalOrder, { resolve, reject })); + .then((paypalOrder: any) => { + const billingAddress = formatPaypalOrderContatcToAdyenFormat(paypalOrder?.payer); + const deliveryAddress = formatPaypalOrderContatcToAdyenFormat( + paypalOrder?.purchase_units?.[0].shipping, + true + ); + + this.setState({ + authorizedEvent: paypalOrder, + ...(billingAddress && { billingAddress }), + ...(deliveryAddress && { deliveryAddress }) + }); + + return new Promise<void>((resolve, reject) => + onAuthorized( + { + authorizedEvent: paypalOrder, + ...(billingAddress && { billingAddress }), + ...(deliveryAddress && { deliveryAddress }) + }, + { resolve, reject } + ) + ); }) .then(() => this.handleAdditionalDetails(state)) - .catch(error => this.handleError(new AdyenCheckoutError('ERROR', 'Something went wrong while parsing PayPal Order', { cause: error }))); + .catch(error => + this.handleError( + new AdyenCheckoutError('ERROR', 'Something went wrong while parsing PayPal Order', { cause: error }) + ) + ); }; handleResolve(token: string) { @@ -142,7 +162,7 @@ class PaypalElement extends UIElement<PayPalConfiguration> { this.componentRef = ref; }} {...this.props} - onCancel={this.handleCancel} + onCancel={() => this.handleError(new AdyenCheckoutError('CANCEL'))} onChange={this.setState} onApprove={this.handleOnApprove} onError={error => { diff --git a/packages/lib/src/components/PayPal/defaultProps.ts b/packages/lib/src/components/PayPal/defaultProps.ts index db140fa7cb..8772547731 100644 --- a/packages/lib/src/components/PayPal/defaultProps.ts +++ b/packages/lib/src/components/PayPal/defaultProps.ts @@ -64,7 +64,6 @@ const defaultProps: Partial<PayPalConfiguration> = { // Events onInit: () => {}, onClick: () => {}, - onCancel: () => {}, onError: () => {}, onShippingChange: () => {} }; diff --git a/packages/lib/src/components/PayPal/types.ts b/packages/lib/src/components/PayPal/types.ts index 232c5c2df7..5c4d9896d0 100644 --- a/packages/lib/src/components/PayPal/types.ts +++ b/packages/lib/src/components/PayPal/types.ts @@ -1,5 +1,4 @@ -import { PaymentAmount, PaymentMethod, ShopperDetails } from '../../types/global-types'; -import UIElement from '../internal/UIElement/UIElement'; +import { AddressData, PaymentAmount, PaymentMethod } from '../../types/global-types'; import { SUPPORTED_LOCALES } from './config'; import { UIElementProps } from '../internal/UIElement/types'; @@ -169,12 +168,15 @@ export interface PayPalConfig { } export interface PayPalConfiguration extends PayPalCommonProps, UIElementProps { - onSubmit?: (state: any, element: UIElement) => void; - onComplete?: (state, element?: UIElement) => void; - onAdditionalDetails?: (state: any, element: UIElement) => void; - onCancel?: (state: any, element: UIElement) => void; - onError?: (state: any, element?: UIElement) => void; - onShopperDetails?(shopperDetails: ShopperDetails, rawData: any, actions: { resolve: () => void; reject: () => void }): void; + /** + * Callback called when PayPal authorizes the payment. + * Must be resolved/rejected with the action object. If resolved, the additional details will be invoked. Otherwise it will be skipped + */ + onAuthorized?: ( + data: { authorizedEvent: any; billingAddress?: Partial<AddressData>; deliveryAddress?: Partial<AddressData> }, + actions: { resolve: () => void; reject: () => void } + ) => void; + paymentMethods?: PaymentMethod[]; showPayButton?: boolean; } diff --git a/packages/lib/src/components/PayPal/utils/create-shopper-details.test.ts b/packages/lib/src/components/PayPal/utils/create-shopper-details.test.ts deleted file mode 100644 index aa83da700f..0000000000 --- a/packages/lib/src/components/PayPal/utils/create-shopper-details.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createShopperDetails } from './create-shopper-details'; -import { ShopperDetails } from '../../../types'; - -const expectedShopperDetails: ShopperDetails = { - shopperName: { firstName: 'Test', lastName: 'User' }, - shopperEmail: 'shopper@example.com', - telephoneNumber: '7465704409', - countryCode: 'US', - billingAddress: { street: '1 Main St', stateOrProvince: 'CA', city: 'San Jose', postalCode: '95131', country: 'US' }, - shippingAddress: { street: '6-50 Simon Carmiggeltstraat', city: 'Amsterdam', postalCode: '1011DJ', country: 'NL' } -}; - -const paypalOrderData = { - create_time: '2023-03-09T10:08:54Z', - id: '7KN17760594192819', - intent: 'AUTHORIZE', - status: 'APPROVED', - payer: { - email_address: 'shopper@example.com', - payer_id: '8QNW9UFUKS9ZC', - address: { - address_line_1: '1 Main St', - admin_area_2: 'San Jose', - admin_area_1: 'CA', - postal_code: '95131', - country_code: 'US' - }, - name: { - given_name: 'Test', - surname: 'User' - }, - phone: { - phone_type: 'HOME', - phone_number: { - national_number: '7465704409' - } - } - }, - purchase_units: [ - { - reference_id: 'default', - custom_id: 'TestMerchantCheckout:pub.v2.8115658705713940.a4Emqe6G-EueTjFogWUNHbARjs036ujUj8pgkf0Qbnw:645042:N6SWRQ7CQGNG5S82:paypal', - invoice_id: 'N6SWRQ7CQGNG5S82', - soft_descriptor: '150-checkout-component', - amount: { - value: '259.00', - currency_code: 'USD' - }, - payee: { - email_address: 'seller_1306503918_biz@adyen.com', - merchant_id: 'QSXMR9W7GV8NY', - display_data: { - brand_name: 'TestMerchantCheckout' - } - }, - shipping: { - name: { - full_name: 'Jaap Stam' - }, - address: { - address_line_1: '6-50 Simon Carmiggeltstraat', - admin_area_2: 'Amsterdam', - postal_code: '1011DJ', - country_code: 'NL' - } - } - } - ] -}; - -test('should format Paypal Order v2 payload to Adyen ShopperDetails format ', () => { - expect(createShopperDetails(paypalOrderData)).toEqual(expectedShopperDetails); -}); - -test('should return null if Paypal order is empty', () => { - expect(createShopperDetails({})).toBeNull(); - expect(createShopperDetails(null)).toBeNull(); - expect(createShopperDetails(undefined)).toBeNull(); -}); diff --git a/packages/lib/src/components/PayPal/utils/create-shopper-details.ts b/packages/lib/src/components/PayPal/utils/create-shopper-details.ts deleted file mode 100644 index edbd788669..0000000000 --- a/packages/lib/src/components/PayPal/utils/create-shopper-details.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { AddressData, ShopperDetails } from '../../../types/global-types'; - -/** - * Parses the Order data from PayPal, and create the shopper details object according to how Adyen expects - */ -const createShopperDetails = (order: any): ShopperDetails | null => { - if (!order) { - return null; - } - - const shopperName = { - firstName: order?.payer?.name?.given_name, - lastName: order?.payer?.name?.surname - }; - const shopperEmail = order?.payer?.email_address; - const countryCode = order?.payer?.address?.country_code; - const telephoneNumber = order?.payer?.phone?.phone_number?.national_number; - const dateOfBirth = order?.payer?.birth_date; - - const billingAddress = mapPayPalAddressToAdyenAddressFormat({ - paypalAddressObject: order?.payer?.address - }); - const shippingAddress = mapPayPalAddressToAdyenAddressFormat({ - paypalAddressObject: order?.purchase_units?.[0].shipping?.address - }); - - const shopperDetails = { - ...(shopperName.firstName && { shopperName }), - ...(shopperEmail && { shopperEmail }), - ...(dateOfBirth && { dateOfBirth }), - ...(telephoneNumber && { telephoneNumber }), - ...(countryCode && { countryCode }), - ...(billingAddress && { billingAddress }), - ...(shippingAddress && { shippingAddress }) - }; - - return Object.keys(shopperDetails).length > 0 ? shopperDetails : null; -}; - -const mapPayPalAddressToAdyenAddressFormat = ({ paypalAddressObject }): Partial<AddressData> | null => { - const getStreet = (addressPart1 = null, addressPart2 = null): string | null => { - if (addressPart1 && addressPart2) return `${addressPart1}, ${addressPart2}`; - if (addressPart1) return addressPart1; - if (addressPart2) return addressPart2; - return null; - }; - - if (!paypalAddressObject) return null; - - const street = getStreet(paypalAddressObject.address_line_1, paypalAddressObject.address_line_2); - - const address = { - ...(street && { street }), - ...(paypalAddressObject.admin_area_1 && { stateOrProvince: paypalAddressObject.admin_area_1 }), - ...(paypalAddressObject.admin_area_2 && { city: paypalAddressObject.admin_area_2 }), - ...(paypalAddressObject.postal_code && { postalCode: paypalAddressObject.postal_code }), - ...(paypalAddressObject.country_code && { country: paypalAddressObject.country_code }) - }; - - return Object.keys(address).length > 0 ? address : null; -}; - -export { createShopperDetails }; diff --git a/packages/lib/src/components/PayPal/utils/format-paypal-order-contact-to-adyen-format.ts b/packages/lib/src/components/PayPal/utils/format-paypal-order-contact-to-adyen-format.ts new file mode 100644 index 0000000000..753d3a38d7 --- /dev/null +++ b/packages/lib/src/components/PayPal/utils/format-paypal-order-contact-to-adyen-format.ts @@ -0,0 +1,33 @@ +import { AddressData } from '../../../types/global-types'; + +/** + * This function formats PayPal contact format to Adyen address format + */ +export const formatPaypalOrderContatcToAdyenFormat = ( + paymentContact: any, + isDeliveryAddress?: boolean +): AddressData | null => { + const getStreet = (addressPart1 = null, addressPart2 = null): string | null => { + if (addressPart1 && addressPart2) return `${addressPart1}, ${addressPart2}`; + if (addressPart1) return addressPart1; + if (addressPart2) return addressPart2; + return null; + }; + + if (!paymentContact || !paymentContact.address) return null; + + const { address, name } = paymentContact; + const street = getStreet(address.address_line_1, address.address_line_2); + + return { + houseNumberOrName: 'ZZ', + ...(street && { street }), + ...(address.admin_area_1 && { stateOrProvince: address.admin_area_1 }), + ...(address.admin_area_2 && { city: address.admin_area_2 }), + ...(address.postal_code && { postalCode: address.postal_code }), + ...(address.country_code && { country: address.country_code }), + ...(isDeliveryAddress && { + firstName: name.full_name + }) + }; +}; diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index c4117c9300..9904cb8c98 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -92,15 +92,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> return Promise.resolve(); } - // ApplePayJS.ApplePayPayment | google.payments.api.PaymentData | undefined - /** - * Certain payment methods have data returned after the shopper authorization step (Ex: GooglePay, ApplePay) - * This getter returns the event data in case it is available - */ - public get authorizedEvent(): any { - return this.state.authorizedEvent; - } - public setState(newState: object): void { this.state = { ...this.state, ...newState }; this.onChange(); diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index a76289dbb7..97cacfbce5 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -368,7 +368,11 @@ export interface RawPaymentResponse extends PaymentResponseData { [key: string]: any; } -export type ActionDescriptionType = 'qr-code-loaded' | 'polling-started' | 'fingerprint-iframe-loaded' | 'challenge-iframe-loaded'; +export type ActionDescriptionType = + | 'qr-code-loaded' + | 'polling-started' + | 'fingerprint-iframe-loaded' + | 'challenge-iframe-loaded'; export interface ActionHandledReturnObject { componentType: string; diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index 7e092aa8cf..65f3e361b1 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -38,8 +38,7 @@ export async function initManual() { environment: process.env.__CLIENT_ENV__, onSubmit: async (state, component, actions) => { - const { authorizedData } = state; - console.log('authorizedData', authorizedData); + console.log('onSubmit', state, component.authorizedEvent); try { const result = await makePayment(state.data); @@ -95,6 +94,10 @@ export async function initManual() { // actions.resolve({ resultCode: result.resultCode }); }, + onChange(state, element) { + console.log('onChange', state, element); + }, + onPaymentCompleted(result, element) { console.log('onPaymentCompleted', result, element); }, @@ -210,30 +213,40 @@ export async function initManual() { return Promise.resolve(true); } - const dropin = new Dropin({ + const gpay = new GooglePay({ core: checkout, - paymentMethodComponents: [Card, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay], - instantPaymentTypes: ['googlepay'], - paymentMethodsConfiguration: { - card: { - challengeWindowSize: '03', - enableStoreDetails: true, - hasHolderName: true, - holderNameRequired: true - }, - paywithgoogle: { - buttonType: 'plain' - }, - klarna: { - useKlarnaWidget: true - } - // storedCard: { - // hideCVC: true - // } - } + shippingAddressRequired: true, + shippingAddressParameters: { + phoneNumberRequired: true + }, + + billingAddressRequired: true }).mount('#dropin-container'); + // const dropin = new Dropin({ + // core: checkout, + // paymentMethodComponents: [Card, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay], + // instantPaymentTypes: ['googlepay'], + // paymentMethodsConfiguration: { + // card: { + // challengeWindowSize: '03', + // enableStoreDetails: true, + // hasHolderName: true, + // holderNameRequired: true + // }, + // paywithgoogle: { + // buttonType: 'plain' + // }, + // klarna: { + // useKlarnaWidget: true + // } + // // storedCard: { + // // hideCVC: true + // // } + // } + // }).mount('#dropin-container'); + handleRedirectResult(); - return [checkout, dropin]; + return [checkout, gpay]; } diff --git a/packages/playground/src/pages/Dropin/session.js b/packages/playground/src/pages/Dropin/session.js index e80d853e58..f786939bc1 100644 --- a/packages/playground/src/pages/Dropin/session.js +++ b/packages/playground/src/pages/Dropin/session.js @@ -46,8 +46,13 @@ export async function initSession() { instantPaymentTypes: ['googlepay'], paymentMethodComponents: [Card, WeChat, Giftcard, PayPal, Ach, GooglePay], paymentMethodsConfiguration: { - paywithgoogle: { - buttonType: 'plain' + googlepay: { + buttonType: 'plain', + + onAuthorized(data, actions) { + console.log(data, actions); + actions.reject(); + } }, card: { hasHolderName: true, diff --git a/packages/playground/src/pages/Wallets/Wallets.js b/packages/playground/src/pages/Wallets/Wallets.js index 96ee37a6c5..28c95fa6bc 100644 --- a/packages/playground/src/pages/Wallets/Wallets.js +++ b/packages/playground/src/pages/Wallets/Wallets.js @@ -142,13 +142,11 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = // PAYPAL window.paypalButtons = new PayPal({ core: window.checkout, - onShopperDetails: (shopperDetails, rawData, actions) => { - console.log('Shopper details', shopperDetails); - console.log('Raw data', rawData); + onAuthorized(data, actions) { + console.log('onAuthorized', data, actions); actions.resolve(); }, onError: (error, component) => { - component.setStatus('ready'); console.log('paypal onError', error); } }).mount('.paypal-field'); @@ -160,7 +158,11 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = environment: 'TEST', // Callbacks - onAuthorized: console.info, + onAuthorized(data, actions) { + console.log(data, actions); + actions.resolve(); + }, + // onError: console.error, // Payment info @@ -175,8 +177,11 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = // Shopper info (optional) emailRequired: true, - shippingAddressRequired: true, - shippingAddressParameters: {}, // https://developers.google.com/pay/api/web/reference/object#ShippingAddressParameters + + // billingAddressRequired: true, + + // shippingAddressRequired: true, + // shippingAddressParameters: {}, // https://developers.google.com/pay/api/web/reference/object#ShippingAddressParameters // Button config (optional) buttonType: 'long', // https://developers.google.com/pay/api/web/reference/object#ButtonOptions From a4aa928d35eac3a1d134c8c9ae6ae0c5cf2ee334 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Tue, 19 Dec 2023 09:49:04 -0300 Subject: [PATCH 22/55] feat: handling failure --- .../lib/src/components/ApplePay/ApplePay.tsx | 4 +- .../src/components/GooglePay/GooglePay.tsx | 2 + .../internal/UIElement/UIElement.tsx | 53 +++++++--- .../components/internal/UIElement/types.ts | 14 ++- .../components/internal/UIElement/utils.ts | 2 +- .../lib/src/core/CheckoutSession/types.ts | 1 + packages/lib/src/core/types.ts | 17 +++ packages/lib/src/types/global-types.ts | 13 ++- packages/playground/src/handlers.js | 8 +- .../playground/src/pages/Dropin/manual.js | 100 +++++++----------- .../playground/src/pages/Wallets/Wallets.js | 8 ++ 11 files changed, 127 insertions(+), 95 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index 64195141f2..f176f320fa 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -128,14 +128,14 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { }) .catch((error: onSubmitReject) => { console.error(error); - - this.setElementStatus('ready'); const errors = error?.error?.applePayError; reject({ status: ApplePaySession.STATUS_FAILURE, errors: errors ? (Array.isArray(errors) ? errors : [errors]) : undefined }); + + this.handleFailedResult(error); }); } }); diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index a6fe87b032..9be4309ca9 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -128,6 +128,8 @@ class GooglePay extends UIElement<GooglePayConfiguration> { reason: error?.error?.googlePayError?.reason || 'OTHER_ERROR' } }); + + this.handleFailedResult(error); }); }); }; diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 9904cb8c98..bdb66ea58d 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -119,16 +119,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> .then(this.sanitizeResponse) .then(this.verifyPaymentDidNotFail) .then(this.handleResponse) - .catch((exception: OnPaymentFailedData) => { - // two scenarios when code reaches here: - // - adv flow: merchant used reject passing error back or empty object - // - adv flow: merchant resolved passed resultCode: Refused,Cancelled,etc - // - on sessions, payment failed with resultCode Refused, Cancelled, etcthis. - this.props.onPaymentFailed?.(exception, this.elementRef); - if (this.props.setStatusAutomatically) { - this.setElementStatus('error'); - } - }); + .catch(this.handleFailedResult); } /** @@ -176,12 +167,22 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> let paymentsResponse: CheckoutSessionPaymentResponse = null; try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars paymentsResponse = await this.core.session.submitPayment(data); } catch (error) { this.handleError(error); return Promise.reject(error); } + // // Uncomment to simulate failed + // return { + // resultCode: 'Refused', + // sessionData: + // 'Ab02b4c0!BQABAgBKGgqfEz8uQlU4yCIOWjA8bkEwmbJ7Qt4r+x5IPXREu1rMjwNk5MDoHFNlv+MWvinS6nXIDniXgRzXCdSC4ksw9CNDBAjOa+B88wRoj/rLTieuWh/0leR88qkV24vtIkjsIsbJTDB78Pd8wX8MEDsXhaAdEIyX9E8eqxuQ3bwPbvLs1Dlgo1ZrfkQRzaNiuVM8ejRG0IWE1bGThJzY+sJvZZHvlDMXIlxhZcDoQvsMj/WwE6+nFJxBiC3oRzmvVn3AbkLQGtvwq16UUSfYbPzG9dXypJMtcrZAQYq2g/2+BSibCcmee9AXq/wij11BERrYmjbDt5NkkdUnDVgAB7pdqbnWX0A2sxBKeYtLSP2kxp+5LoU/Wty3fmcVA3VKVkHfgmIihkeL8lY++5hvHjnkzOE4tyx/sheiKS4zqoWE43TD6n8mpFskAzwMHq4G2o6vkXqvaKFEq7y/R2fVrCypenmRhkPASizpM265rKLU+L4E/C+LMHfN0LYKRMCrLr0gI2GAp+1PZLHgh0tCtiJC/zcJJtJs6sHNQxLUN+kxJuELUHOcuL3ivjG+mWteUnBENZu7KqOSZYetiWYRiyLOXDiBHqbxuQwTuO54L15VLkS/mYB20etibM1nn+fRmbo+1IJkCSalhwi5D7fSrpjbQTmAsOpJT1N8lC1MSNmAvAwG1kWL4JxYwXDKYyYASnsia2V5IjoiQUYwQUFBMTAzQ0E1MzdFQUVEODdDMjRERDUzOTA5QjgwQTc4QTkyM0UzODIzRDY4REFDQzk0QjlGRjgzMDVEQyJ98uZI4thGveOByYbomCeeP2Gy2rzs99FOBoDYVeWIUjyM+gfnW89DdJZAhxe74Tv0TnL5DRQYPCTRQPOoLbQ21NaeSho70FNE+n8XYKlVK5Ore6BoB6IVCaal5MkM27VmZPMmGflgcPx+pakx+EmRsYGdvYNImYxJYrRk3CI+l3T3ZiVpPPqebaVSLaSkEfu0iOFPjjLUhWN6QW6c18heE5vq/pcoeBf7p0Jgr9I5aBFY0avYG57BDGHzU1ZiQ9LLMTis2BA7Ap9pdNq8FVXL4fnoVHNZiiANOf3uvSknPKBID8sdOXUStA0crmO322FYjDqh1n6FG+D7+OJSayNsXIz6Zoy0eFn4HbT8nt8L2X2tdzkMayCYHXRwKh13Xyleqxt4WoEZmhwTmB3p9d1F0SylWnjcC6o/DnshJ9mMW/8D3oWS30Z7BwRODqKGVahRD0YGRzwMbVnEe5JFRfNvJZdLGl35L9632DVmuFQ0lr/8WNL/NrAJNtI6PXrZMNiza0/omPwPfe5ZYuD1Jgq59TX4h9d+3fdkArcJYL7AdoMZON1YEiWY5EzazQwtHd9yzdty9ZHPxAfuOfCh4OhbhFNp+v5YQ+PzKZ+UpM1VxV863+9XgWEURPNvX7qq1cpUSRzrSGq01QBBM3MKzRh5mAgqIdXgtl7L0EXAep0MECc7QY0/o3tW3VR8eEJGsSzrNxpFItqj0SEaIWo25dRfkl5zuw47GQrN9Qzxl2WV3A38MQPUqFtIr/71Rjkphgg49ZGWEYCwgFmm8jJc2/5qTabSGk4bzwiETCTzeydq30bUGqCwglj8CrFViAuQeTJm7dp+PYKMkUNvQRpnSXMj6Kz7rvAMzhzJgK62ltN2idqKxLC7WtivCUgejuQUvNreCYBQCaKwTwP02lZsJpGF9yw8gbyuoB+2aB7IZmgIB8GP4qVQ/ht5B9z/FLohK/8cSPV/4i32SNNdcwhV', + // sessionResult: + // 'X3XtfGC7!H4sIAAAAAAAA/6tWykxRslJyDjaxNDMyM3E2MXIyNDUys3RU0lHKTS1KzkjMK3FMTs4vzSsBKgtJLS7xhYo6Z6QmZ+eXlgAVFpcklpQWA+WLUtNKi1NTlGoBMEEbz1cAAAA=iMsCaEJ5LcnsqIUtmNxjm8HtfQ8gZW8JewEU3wHz4qg=' + // }; + return paymentsResponse; } @@ -301,15 +302,37 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> }); }; - protected handleSuccessResult = (result: PaymentResponseData) => { + /** + * Handles when the payment fails. The payment fails when: + * - adv flow: the merchant rejects the payment + * - adv flow: the merchant resolves the payment with a failed resultCode + * - sessions: an error occurs during session when making the payment + * - sessions: the payment fail + * + * @param result + */ + protected handleFailedResult = (result: OnPaymentFailedData): void => { if (this.props.setStatusAutomatically) { - this.setElementStatus('success'); + this.setElementStatus('error'); } - if (this.props.onPaymentCompleted) { - this.props.onPaymentCompleted(result, this.elementRef); + this.props.onPaymentFailed?.(result, this.elementRef); + }; + + protected handleSuccessResult = (result: PaymentResponseData): void => { + const sanitizeResult = (result: PaymentResponseData) => { + delete result.order; + delete result.action; + if (!result.donationToken || result.donationToken.length === 0) delete result.donationToken; + }; + + if (this.props.setStatusAutomatically) { + this.setElementStatus('success'); } - return result; + + sanitizeResult(result); + + this.props.onPaymentCompleted?.(result, this.elementRef); }; /** diff --git a/packages/lib/src/components/internal/UIElement/types.ts b/packages/lib/src/components/internal/UIElement/types.ts index d5db40edbb..fdf087805c 100644 --- a/packages/lib/src/components/internal/UIElement/types.ts +++ b/packages/lib/src/components/internal/UIElement/types.ts @@ -1,7 +1,12 @@ import { h } from 'preact'; import Session from '../../../core/CheckoutSession'; import UIElement from './UIElement'; -import { ActionHandledReturnObject, PaymentAction, PaymentAmount, PaymentAmountExtended } from '../../../types/global-types'; +import { + ActionHandledReturnObject, + PaymentAction, + PaymentAmount, + PaymentAmountExtended +} from '../../../types/global-types'; import Language from '../../../language'; import { BaseElementProps, IBaseElement } from '../BaseElement/types'; import { PayButtonProps } from '../PayButton/PayButton'; @@ -9,8 +14,10 @@ import { CoreConfiguration, ICore } from '../../../core/types'; export type PayButtonFunctionProps = Omit<PayButtonProps, 'amount'>; -// TODO add onPaymentCompleted -type CoreCallbacks = Pick<CoreConfiguration, 'onSubmit' | 'onPaymentFailed' | 'onOrderUpdated' | 'onPaymentMethodsRequest'>; +type CoreCallbacks = Pick< + CoreConfiguration, + 'onSubmit' | 'onPaymentFailed' | 'onPaymentCompleted' | 'onOrderUpdated' | 'onPaymentMethodsRequest' +>; export type UIElementProps = BaseElementProps & CoreCallbacks & { @@ -24,7 +31,6 @@ export type UIElementProps = BaseElementProps & onActionHandled?: (rtnObj: ActionHandledReturnObject) => void; onAdditionalDetails?: (state: any, element: UIElement) => void; onError?: (error, element?: UIElement) => void; - onPaymentCompleted?: (result: any, element: UIElement) => void; beforeRedirect?: (resolve, reject, redirectData, element: UIElement) => void; isInstantPayment?: boolean; diff --git a/packages/lib/src/components/internal/UIElement/utils.ts b/packages/lib/src/components/internal/UIElement/utils.ts index 7af89b9ff8..dca466ac28 100644 --- a/packages/lib/src/components/internal/UIElement/utils.ts +++ b/packages/lib/src/components/internal/UIElement/utils.ts @@ -1,7 +1,7 @@ import { UIElementStatus } from './types'; import { RawPaymentResponse, PaymentResponseData } from '../../../types/global-types'; -const ALLOWED_PROPERTIES = ['action', 'resultCode', 'sessionData', 'order', 'sessionResult']; +const ALLOWED_PROPERTIES = ['action', 'resultCode', 'sessionData', 'order', 'sessionResult', 'donationToken']; export function getSanitizedResponse(response: RawPaymentResponse): PaymentResponseData { const removedProperties = []; diff --git a/packages/lib/src/core/CheckoutSession/types.ts b/packages/lib/src/core/CheckoutSession/types.ts index 592327e534..5bfa0f40a4 100644 --- a/packages/lib/src/core/CheckoutSession/types.ts +++ b/packages/lib/src/core/CheckoutSession/types.ts @@ -32,6 +32,7 @@ export type CheckoutSessionSetupResponse = { export type CheckoutSessionPaymentResponse = { sessionData: string; + sessionResult: string; status?: string; resultCode: ResultCode; action?: PaymentAction; diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index b4d7b098d9..abb087a2ea 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -174,8 +174,25 @@ export interface CoreConfiguration { } ): Promise<void>; + /** + * Called when the payment succeeds. + * + * The first parameter is the sessions response (when using sessions flow), or the result code. + * + * @param data + * @param element + */ onPaymentCompleted?(data: OnPaymentCompletedData, element?: UIElement): void; + /** + * Called when the payment fails. + * + * The first parameter is poppulated when merchant is using sessions, or when the payment was rejected + * with an object. (Ex: 'action.reject(obj)' ). Otherwise, it will be empty. + * + * @param data + * @param element + */ onPaymentFailed?(data?: OnPaymentFailedData, element?: UIElement): void; onSubmit?( diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index 97cacfbce5..e78fc5699f 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -330,17 +330,14 @@ export type ResultCode = | 'RedirectShopper' | 'Refused'; -export interface OnPaymentCompletedData { +export type SessionsResponse = { sessionData: string; sessionResult: string; resultCode: ResultCode; -} +}; +export type OnPaymentCompletedData = SessionsResponse | { resultCode: ResultCode }; -export type OnPaymentFailedData = - | OnPaymentCompletedData - | (onSubmitReject & { - resultCode: ResultCode; - }); +export type OnPaymentFailedData = SessionsResponse | onSubmitReject; //TODO double check these values export interface PaymentMethodsRequestData { @@ -353,6 +350,7 @@ export interface PaymentResponseAdvancedFlow { resultCode: ResultCode; action?: PaymentAction; order?: Order; + donationToken?: string; } export interface PaymentResponseData { @@ -362,6 +360,7 @@ export interface PaymentResponseData { sessionData?: string; sessionResult?: string; order?: Order; + donationToken?: string; } export interface RawPaymentResponse extends PaymentResponseData { diff --git a/packages/playground/src/handlers.js b/packages/playground/src/handlers.js index 71a527100b..ccaae50d34 100644 --- a/packages/playground/src/handlers.js +++ b/packages/playground/src/handlers.js @@ -35,7 +35,8 @@ export async function handleSubmit(state, component, actions) { try { const result = await makePayment(state.data); - // happpy flow + if (!result.resultCode) actions.reject(); + if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { actions.reject({ resultCode: result.resultCode, @@ -48,11 +49,12 @@ export async function handleSubmit(state, component, actions) { actions.resolve({ action: result.action, order: result.order, - resultCode: result.resultCode + resultCode: result.resultCode, + donationToken: result.donationToken }); } } catch (error) { - // Something failed in the request + console.error('## onSubmit - critical error', error); actions.reject(); } } diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index 65f3e361b1..1d881dd5e0 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -43,7 +43,8 @@ export async function initManual() { try { const result = await makePayment(state.data); - // happpy flow + if (!result.resultCode) actions.reject(); + if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { actions.reject({ resultCode: result.resultCode, @@ -56,42 +57,14 @@ export async function initManual() { actions.resolve({ action: result.action, order: result.order, - resultCode: result.resultCode + resultCode: result.resultCode, + donationToken: result.donationToken }); } } catch (error) { - // Something failed in the request + console.error('## onSubmit - critical error', error); actions.reject(); } - - // - // - // if (result.order && result.order?.remainingAmount?.value > 0) { - // const order = { - // orderData: result.order.orderData, - // pspReference: result.order.pspReference - // }; - // - // const orderPaymentMethods = await getPaymentMethods({ order, amount, shopperLocale }); - // - // actions.resolve({ - // order, - // paymentMethodsResponse: orderPaymentMethods - // }); - // - // return; - // } - - // - // // Trigger Error for GooglePay - // // actions.reject({ - // // googlePayError: { - // // message: 'Not sufficient funds', - // // reason: 'OTHER_ERROR,' - // // } - // // }); - // - // actions.resolve({ resultCode: result.resultCode }); }, onChange(state, element) { @@ -104,6 +77,7 @@ export async function initManual() { onPaymentFailed(result, element) { console.log('onPaymentFailed', result, element); }, + onAdditionalDetails: async (state, component) => { const result = await makeDetailsCall(state.data); @@ -213,40 +187,40 @@ export async function initManual() { return Promise.resolve(true); } - const gpay = new GooglePay({ - core: checkout, - shippingAddressRequired: true, - shippingAddressParameters: { - phoneNumberRequired: true - }, - - billingAddressRequired: true - }).mount('#dropin-container'); - - // const dropin = new Dropin({ + // const gpay = new GooglePay({ // core: checkout, - // paymentMethodComponents: [Card, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay], - // instantPaymentTypes: ['googlepay'], - // paymentMethodsConfiguration: { - // card: { - // challengeWindowSize: '03', - // enableStoreDetails: true, - // hasHolderName: true, - // holderNameRequired: true - // }, - // paywithgoogle: { - // buttonType: 'plain' - // }, - // klarna: { - // useKlarnaWidget: true - // } - // // storedCard: { - // // hideCVC: true - // // } - // } + // shippingAddressRequired: true, + // shippingAddressParameters: { + // phoneNumberRequired: true + // }, + + // billingAddressRequired: true // }).mount('#dropin-container'); + const dropin = new Dropin({ + core: checkout, + paymentMethodComponents: [Card, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay], + instantPaymentTypes: ['googlepay'], + paymentMethodsConfiguration: { + card: { + challengeWindowSize: '03', + enableStoreDetails: true, + hasHolderName: true, + holderNameRequired: true + }, + paywithgoogle: { + buttonType: 'plain' + }, + klarna: { + useKlarnaWidget: true + } + // storedCard: { + // hideCVC: true + // } + } + }).mount('#dropin-container'); + handleRedirectResult(); - return [checkout, gpay]; + return [checkout]; } diff --git a/packages/playground/src/pages/Wallets/Wallets.js b/packages/playground/src/pages/Wallets/Wallets.js index 28c95fa6bc..d912669053 100644 --- a/packages/playground/src/pages/Wallets/Wallets.js +++ b/packages/playground/src/pages/Wallets/Wallets.js @@ -22,6 +22,14 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = onError(error) { console.log(error); }, + + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + }, + showPayButton: true }); From 75ddaa3c1decedc4b87542a0719ee3ecb5e9ccda Mon Sep 17 00:00:00 2001 From: antoniof <m1aw@users.noreply.github.com> Date: Tue, 19 Dec 2023 20:34:52 +0000 Subject: [PATCH 23/55] feat: giftcard in storybook --- .../helpers/create-advanced-checkout.ts | 43 ++++++++--- .../stories/giftcards/Gifcards.stories.tsx | 56 ++------------ .../stories/giftcards/GiftcardExample.tsx | 75 +++++++++++++++++++ yarn.lock | 7 +- 4 files changed, 117 insertions(+), 64 deletions(-) create mode 100644 packages/lib/storybook/stories/giftcards/GiftcardExample.tsx diff --git a/packages/lib/storybook/helpers/create-advanced-checkout.ts b/packages/lib/storybook/helpers/create-advanced-checkout.ts index fda07a31b4..c2f3efc25f 100644 --- a/packages/lib/storybook/helpers/create-advanced-checkout.ts +++ b/packages/lib/storybook/helpers/create-advanced-checkout.ts @@ -1,6 +1,6 @@ import { AdyenCheckout } from '../../src/index'; -import { cancelOrder, checkBalance, createOrder, getPaymentMethods } from './checkout-api-calls'; -import { handleAdditionalDetails, handleChange, handleError, handleSubmit } from './checkout-handlers'; +import { cancelOrder, checkBalance, createOrder, getPaymentMethods, makePayment } from './checkout-api-calls'; +import { handleAdditionalDetails, handleChange, handleError, handleFinalState } from './checkout-handlers'; import getCurrency from '../utils/get-currency'; import { AdyenCheckoutProps } from '../stories/types'; import Checkout from '../../src/core/core'; @@ -32,13 +32,34 @@ async function createAdvancedFlowCheckout({ locale: shopperLocale, showPayButton, - onSubmit: (state, component) => { - const paymentData = { - amount: paymentAmount, - countryCode, - shopperLocale - }; - handleSubmit(state, component, checkout, paymentData); + onSubmit: async (state, component, actions) => { + try { + const paymentData = { + amount: paymentAmount, + countryCode, + shopperLocale + }; + + const result = await makePayment(state.data, paymentData); + + // happpy flow + if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { + actions.reject({ + error: { + googlePayError: {} + } + }); + } else { + actions.resolve({ + action: result.action, + order: result.order, + resultCode: result.resultCode + }); + } + } catch (error) { + // Something failed in the request + actions.reject(); + } }, onChange: (state, component) => { @@ -82,6 +103,10 @@ async function createAdvancedFlowCheckout({ onError: (error, component) => { handleError(error, component); + }, + + onPaymentCompleted: (result, component) => { + handleFinalState(result, component); } }); diff --git a/packages/lib/storybook/stories/giftcards/Gifcards.stories.tsx b/packages/lib/storybook/stories/giftcards/Gifcards.stories.tsx index 49f07220f1..0e9143139a 100644 --- a/packages/lib/storybook/stories/giftcards/Gifcards.stories.tsx +++ b/packages/lib/storybook/stories/giftcards/Gifcards.stories.tsx @@ -1,66 +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 { ANCVConfiguration } from '../../../src/components/ANCV/types'; -import Giftcard from '../../../src/components/Giftcard'; import { GiftCardConfiguration } from '../../../src/components/Giftcard/types'; -import { makePayment } from '../../helpers/checkout-api-calls'; +import { GiftcardExample } from './GiftcardExample'; type GifcardStory = StoryObj<PaymentMethodStoryProps<GiftCardConfiguration>>; -const meta: Meta<PaymentMethodStoryProps<ANCVConfiguration>> = { - title: 'Giftcards/Generic Giftcard' +const meta: Meta<PaymentMethodStoryProps<GiftCardConfiguration>> = { + title: 'Partial Payments/Givex(Giftcard) with Card' }; export const Default: GifcardStory = { - render: (args, context) => { - const { componentConfiguration } = args; - const checkout = getStoryContextCheckout(context); - const ancv = new Giftcard({ core: checkout, ...componentConfiguration }); - return <Container element={ancv} />; + render: args => { + return <GiftcardExample contextArgs={args} />; }, args: { countryCode: 'NL', - amount: 200000, + amount: 20000, useSessions: false, componentConfiguration: { - brand: 'genericgiftcard', - onSubmit: async (state, element, actions) => { - try { - const paymentData = { - amount: { - value: 200000, - currency: 'EUR' - }, - countryCode: 'NL', - shopperLocale: 'en-GB' - }; - const result = await makePayment(state.data, paymentData); - - // happpy flow - if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { - actions.reject({ - error: { - googlePayError: {} - } - }); - } else { - actions.resolve({ - action: result.action, - order: result.order, - resultCode: result.resultCode - }); - } - } catch (error) { - // Something failed in the request - actions.reject(); - } - }, - onOrderUpdated(data) { - // TODO render another component - alert(JSON.stringify(data)); - } + brand: 'givex' } } }; diff --git a/packages/lib/storybook/stories/giftcards/GiftcardExample.tsx b/packages/lib/storybook/stories/giftcards/GiftcardExample.tsx new file mode 100644 index 0000000000..3f20b7edd5 --- /dev/null +++ b/packages/lib/storybook/stories/giftcards/GiftcardExample.tsx @@ -0,0 +1,75 @@ +import { useEffect, useRef, useState } from 'preact/hooks'; +import { createSessionsCheckout } from '../../helpers/create-sessions-checkout'; +import { createAdvancedFlowCheckout } from '../../helpers/create-advanced-checkout'; +import Giftcard from '../../../src/components/Giftcard'; +import Card from '../../../src/components/Card'; +import { GiftCardConfiguration } from '../../../src/components/Giftcard/types'; +import { PaymentMethodStoryProps } from '../types'; + +interface GiftcardExampleProps { + contextArgs: PaymentMethodStoryProps<GiftCardConfiguration>; +} + +export const GiftcardExample = ({ contextArgs }: GiftcardExampleProps) => { + const container = useRef(null); + const checkout = useRef(null); + const [element, setElement] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + + const createCheckout = async () => { + const { useSessions, showPayButton, countryCode, shopperLocale, amount } = contextArgs; + + checkout.current = useSessions + ? await createSessionsCheckout({ showPayButton, countryCode, shopperLocale, amount }) + : await createAdvancedFlowCheckout({ + showPayButton, + countryCode, + shopperLocale, + amount + }); + + const onOrderUpdated = () => { + const card = new Card({ + core: checkout.current, + _disableClickToPay: true + }); + setElement(card); + }; + + const giftcardElement = new Giftcard({ + core: checkout.current, + ...contextArgs.componentConfiguration, + onOrderUpdated: onOrderUpdated + }); + setElement(giftcardElement); + }; + + useEffect(() => { + void createCheckout(); + }, [contextArgs]); + + useEffect(() => { + if (element?.isAvailable) { + element + .isAvailable() + .then(() => { + element.mount(container.current); + }) + .catch(error => { + setErrorMessage(error.toString()); + }); + } else if (element) { + element.mount(container.current); + } + }, [element]); + + return ( + <div> + {errorMessage ? ( + <div>{errorMessage}</div> + ) : ( + <div ref={container} id="component-root" className="component-wrapper" /> + )} + </div> + ); +}; diff --git a/yarn.lock b/yarn.lock index cbc372f86f..8364510b8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13233,16 +13233,11 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.11: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9, regenerator-runtime@^0.14.0: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== - regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" From 83dd8408e40568fbc0948876c16f7a0bd5785b71 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Wed, 20 Dec 2023 14:43:28 -0300 Subject: [PATCH 24/55] additional details part --- .../lib/src/components/ApplePay/ApplePay.tsx | 3 +- .../src/components/GooglePay/GooglePay.tsx | 3 +- .../internal/UIElement/UIElement.tsx | 142 +++++++++--------- .../components/internal/UIElement/types.ts | 8 +- .../internal/UIElement/utils.test.ts | 4 +- .../components/internal/UIElement/utils.ts | 2 +- packages/lib/src/core/core.ts | 7 +- packages/lib/src/core/types.ts | 16 +- .../playground/src/pages/Dropin/manual.js | 55 ++++--- .../playground/src/pages/Dropin/session.js | 2 +- 10 files changed, 138 insertions(+), 104 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index f176f320fa..a89befe775 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -18,6 +18,7 @@ import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; import { onSubmitReject } from '../../core/types'; import { PaymentResponseData } from '../../types/global-types'; +import { sanitizeResponse } from '../internal/UIElement/utils'; const latestSupportedVersion = 14; @@ -113,7 +114,7 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { this.handleAuthorization() .then(this.makePaymentsCall) - .then(this.sanitizeResponse) + .then(sanitizeResponse) .then(this.verifyPaymentDidNotFail) .then(this.collectOrderTrackingDetailsIfNeeded) .then(({ paymentResponse, orderDetails }) => { diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 9be4309ca9..5bad204efb 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -10,6 +10,7 @@ import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; import { onSubmitReject } from '../../core/types'; import { AddressData, PaymentResponseData } from '../../types/global-types'; +import { sanitizeResponse } from '../internal/UIElement/utils'; class GooglePay extends UIElement<GooglePayConfiguration> { public static type = TxVariants.googlepay; @@ -108,7 +109,7 @@ class GooglePay extends UIElement<GooglePayConfiguration> { return new Promise<google.payments.api.PaymentAuthorizationResult>(resolve => { this.handleAuthorization() .then(this.makePaymentsCall) - .then(this.sanitizeResponse) + .then(sanitizeResponse) .then(this.verifyPaymentDidNotFail) .then((paymentResponse: PaymentResponseData) => { resolve({ transactionState: 'SUCCESS' }); diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index bdb66ea58d..f919b9aec1 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -1,7 +1,7 @@ import { h } from 'preact'; import BaseElement from '../BaseElement/BaseElement'; import PayButton from '../PayButton'; -import { getSanitizedResponse } from './utils'; +import { sanitizeResponse } from './utils'; import AdyenCheckoutError from '../../../core/Errors/AdyenCheckoutError'; import { hasOwnProperty } from '../../../utils/hasOwnProperty'; import { CoreConfiguration, ICore } from '../../../core/types'; @@ -9,17 +9,17 @@ import { Resources } from '../../../core/Context/Resources'; import { NewableComponent } from '../../../core/core.registry'; import { ComponentMethodsRef, IUIElement, PayButtonFunctionProps, UIElementProps, UIElementStatus } from './types'; import { + OnPaymentFailedData, + Order, PaymentAction, - PaymentResponseData, PaymentData, - RawPaymentResponse, - PaymentResponseAdvancedFlow, - OnPaymentFailedData, PaymentMethodsResponse, - Order + PaymentResponseAdvancedFlow, + PaymentResponseData, + RawPaymentResponse } from '../../../types/global-types'; import './UIElement.scss'; -import { CheckoutSessionPaymentResponse } from '../../../core/CheckoutSession/types'; +import { CheckoutSessionDetailsResponse, CheckoutSessionPaymentResponse } from '../../../core/CheckoutSession/types'; export abstract class UIElement<P extends UIElementProps = UIElementProps> extends BaseElement<P> @@ -50,12 +50,15 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> this.setState = this.setState.bind(this); this.onValid = this.onValid.bind(this); this.onComplete = this.onComplete.bind(this); - this.makePaymentsCall = this.makePaymentsCall.bind(this); this.handleAction = this.handleAction.bind(this); this.handleOrder = this.handleOrder.bind(this); + this.handleAdditionalDetails = this.handleAdditionalDetails.bind(this); this.handleResponse = this.handleResponse.bind(this); this.setElementStatus = this.setElementStatus.bind(this); + this.makePaymentsCall = this.makePaymentsCall.bind(this); + this.makeAdditionalDetailsCall = this.makeAdditionalDetailsCall.bind(this); + this.submitUsingSessionsFlow = this.submitUsingSessionsFlow.bind(this); this.elementRef = (props && props.elementRef) || this; @@ -97,6 +100,23 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> this.onChange(); } + public showValidation(): this { + if (this.componentRef && this.componentRef.showValidation) this.componentRef.showValidation(); + return this; + } + + public setElementStatus(status: UIElementStatus, props?: any): this { + this.elementRef?.setStatus(status, props); + return this; + } + + public setStatus(status: UIElementStatus, props?): this { + if (this.componentRef?.setStatus) { + this.componentRef.setStatus(status, props); + } + return this; + } + protected onChange(): object { const isValid = this.isValid; const state = { data: this.data, errors: this.state.errors, valid: this.state.valid, isValid }; @@ -106,9 +126,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> return state; } - /** - * Submit payment method data. If the form is not valid, it will trigger validation. - */ public submit(): void { if (!this.isValid) { this.showValidation(); @@ -116,15 +133,12 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> } this.makePaymentsCall() - .then(this.sanitizeResponse) + .then(sanitizeResponse) .then(this.verifyPaymentDidNotFail) .then(this.handleResponse) .catch(this.handleFailedResult); } - /** - * Triggers the payment flow - */ protected makePaymentsCall(): Promise<PaymentResponseAdvancedFlow | CheckoutSessionPaymentResponse> { if (this.props.setStatusAutomatically) { this.setElementStatus('loading'); @@ -147,7 +161,12 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> return beforeSubmitEvent.then(this.submitUsingSessionsFlow); } - this.handleError(new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Could not submit the payment')); + this.handleError( + new AdyenCheckoutError( + 'IMPLEMENTATION_ERROR', + 'Could not perform /payments call. Callback "onSubmit" is missing or Checkout session is not available' + ) + ); } private async submitUsingAdvancedFlow(): Promise<PaymentResponseAdvancedFlow> { @@ -164,11 +183,8 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> } private async submitUsingSessionsFlow(data: PaymentData): Promise<CheckoutSessionPaymentResponse> { - let paymentsResponse: CheckoutSessionPaymentResponse = null; - try { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - paymentsResponse = await this.core.session.submitPayment(data); + return await this.core.session.submitPayment(data); } catch (error) { this.handleError(error); return Promise.reject(error); @@ -182,12 +198,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> // sessionResult: // 'X3XtfGC7!H4sIAAAAAAAA/6tWykxRslJyDjaxNDMyM3E2MXIyNDUys3RU0lHKTS1KzkjMK3FMTs4vzSsBKgtJLS7xhYo6Z6QmZ+eXlgAVFpcklpQWA+WLUtNKi1NTlGoBMEEbz1cAAAA=iMsCaEJ5LcnsqIUtmNxjm8HtfQ8gZW8JewEU3wHz4qg=' // }; - - return paymentsResponse; - } - - protected sanitizeResponse(rawResponse: RawPaymentResponse): PaymentResponseData { - return getSanitizedResponse(rawResponse); } protected verifyPaymentDidNotFail(response: PaymentResponseData): Promise<PaymentResponseData> { @@ -208,40 +218,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> if (this.props.onComplete) this.props.onComplete(state, this.elementRef); } - public showValidation(): this { - if (this.componentRef && this.componentRef.showValidation) this.componentRef.showValidation(); - return this; - } - - public setElementStatus(status: UIElementStatus, props?: any): this { - this.elementRef?.setStatus(status, props); - return this; - } - - public setStatus(status: UIElementStatus, props?): this { - if (this.componentRef?.setStatus) { - this.componentRef.setStatus(status, props); - } - return this; - } - - /** - * Submit the payment using sessions flow - * - * @param data - * @private - */ - private submitPayment(data): Promise<void> { - return this.core.session - .submitPayment(data) - .then(this.handleResponse) - .catch(error => this.handleError(error)); - } - - private submitAdditionalDetails(data): Promise<void> { - return this.core.session.submitDetails(data).then(this.handleResponse).catch(this.handleError); - } - protected handleError = (error: AdyenCheckoutError): void => { /** * Set status using elementRef, which: @@ -255,15 +231,47 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> } }; - protected handleAdditionalDetails = state => { + protected handleAdditionalDetails(state: any): void { + this.makeAdditionalDetailsCall(state) + .then(sanitizeResponse) + .then(this.verifyPaymentDidNotFail) + .then(this.handleResponse) + .catch(this.handleFailedResult); + } + + private makeAdditionalDetailsCall( + state: any + ): Promise<CheckoutSessionDetailsResponse | PaymentResponseAdvancedFlow> { + if (this.props.setStatusAutomatically) { + this.setElementStatus('loading'); + } + if (this.props.onAdditionalDetails) { - this.props.onAdditionalDetails(state, this.elementRef); - } else if (this.props.session) { - this.submitAdditionalDetails(state.data); + return new Promise<PaymentResponseAdvancedFlow>((resolve, reject) => { + this.props.onAdditionalDetails(state, this.elementRef, { resolve, reject }); + }); } - return state; - }; + if (this.props.session) { + return this.submitAdditionalDetailsUsingSessionsFlow(state.data); + } + + this.handleError( + new AdyenCheckoutError( + 'IMPLEMENTATION_ERROR', + 'Could not perform /payments/details call. Callback "onAdditionalDetails" is missing or Checkout session is not available' + ) + ); + } + + private async submitAdditionalDetailsUsingSessionsFlow(data: any): Promise<CheckoutSessionDetailsResponse> { + try { + return this.core.session.submitDetails(data); + } catch (error) { + this.handleError(error); + return Promise.reject(error); + } + } public handleAction(action: PaymentAction, props = {}): UIElement<P> | null { if (!action || !action.type) { @@ -342,7 +350,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> * @param rawResponse - */ protected handleResponse(rawResponse: RawPaymentResponse): void { - const response = getSanitizedResponse(rawResponse); + const response = sanitizeResponse(rawResponse); if (response.action) { this.elementRef.handleAction(response.action); diff --git a/packages/lib/src/components/internal/UIElement/types.ts b/packages/lib/src/components/internal/UIElement/types.ts index fdf087805c..2f57ef45fb 100644 --- a/packages/lib/src/components/internal/UIElement/types.ts +++ b/packages/lib/src/components/internal/UIElement/types.ts @@ -16,7 +16,12 @@ export type PayButtonFunctionProps = Omit<PayButtonProps, 'amount'>; type CoreCallbacks = Pick< CoreConfiguration, - 'onSubmit' | 'onPaymentFailed' | 'onPaymentCompleted' | 'onOrderUpdated' | 'onPaymentMethodsRequest' + | 'onSubmit' + | 'onAdditionalDetails' + | 'onPaymentFailed' + | 'onPaymentCompleted' + | 'onOrderUpdated' + | 'onPaymentMethodsRequest' >; export type UIElementProps = BaseElementProps & @@ -29,7 +34,6 @@ export type UIElementProps = BaseElementProps & onComplete?: (state, element: UIElement) => void; onActionHandled?: (rtnObj: ActionHandledReturnObject) => void; - onAdditionalDetails?: (state: any, element: UIElement) => void; onError?: (error, element?: UIElement) => void; beforeRedirect?: (resolve, reject, redirectData, element: UIElement) => void; diff --git a/packages/lib/src/components/internal/UIElement/utils.test.ts b/packages/lib/src/components/internal/UIElement/utils.test.ts index 48be59824d..a67c7584dd 100644 --- a/packages/lib/src/components/internal/UIElement/utils.test.ts +++ b/packages/lib/src/components/internal/UIElement/utils.test.ts @@ -1,4 +1,4 @@ -import { getSanitizedResponse } from './utils'; +import { sanitizeResponse } from './utils'; describe('components utils', () => { describe('getSanitizedResponse', () => { @@ -13,7 +13,7 @@ describe('components utils', () => { sessionResult: 'XYZ123' }; - const sanitizedResponse = getSanitizedResponse(rawResponse); + const sanitizedResponse = sanitizeResponse(rawResponse); expect(sanitizedResponse.resultCode).toBeTruthy(); expect(sanitizedResponse.sessionResult).toBeTruthy(); expect((sanitizedResponse as any).someBackendProperty).toBeUndefined(); diff --git a/packages/lib/src/components/internal/UIElement/utils.ts b/packages/lib/src/components/internal/UIElement/utils.ts index dca466ac28..7f57ffab5a 100644 --- a/packages/lib/src/components/internal/UIElement/utils.ts +++ b/packages/lib/src/components/internal/UIElement/utils.ts @@ -3,7 +3,7 @@ import { RawPaymentResponse, PaymentResponseData } from '../../../types/global-t const ALLOWED_PROPERTIES = ['action', 'resultCode', 'sessionData', 'order', 'sessionResult', 'donationToken']; -export function getSanitizedResponse(response: RawPaymentResponse): PaymentResponseData { +export function sanitizeResponse(response: RawPaymentResponse): PaymentResponseData { const removedProperties = []; const sanitizedObject = Object.keys(response).reduce((acc, cur) => { diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 7817addeaf..b3542fc246 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -110,9 +110,10 @@ class Core implements ICore { * @param details - */ public submitDetails(details): void { - if (this.options.onAdditionalDetails) { - return this.options.onAdditionalDetails(details); - } + // TODO: Check this + // if (this.options.onAdditionalDetails) { + // return this.options.onAdditionalDetails(details); + // } if (this.session) { this.session diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index abb087a2ea..2fc0c74f55 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -204,7 +204,21 @@ export interface CoreConfiguration { } ): void; - onAdditionalDetails?(state: any, element?: UIElement): void; + /** + * Callback used in the Advanced flow to perform the /payments/details API call. + * + * @param state + * @param element + * @param actions + */ + onAdditionalDetails?( + state: any, + element: UIElement, + actions: { + resolve: (response: PaymentResponseAdvancedFlow) => void; + reject: (error?: onSubmitReject) => void; + } + ): void; onActionHandled?(data: ActionHandledReturnObject): void; diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index 1d881dd5e0..baca6af945 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -47,11 +47,11 @@ export async function initManual() { if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { actions.reject({ - resultCode: result.resultCode, - error: { - googlePayError: {}, - applePayError: {} - } + resultCode: result.resultCode + // error: { + // googlePayError: {}, + // applePayError: {} + // } }); } else { actions.resolve({ @@ -78,26 +78,31 @@ export async function initManual() { console.log('onPaymentFailed', result, element); }, - onAdditionalDetails: async (state, component) => { - const result = await makeDetailsCall(state.data); - - 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); + onAdditionalDetails: async (state, component, actions) => { + try { + const result = await makeDetailsCall(state.data); + + if (!result.resultCode) actions.reject(); + + if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { + actions.reject({ + resultCode: result.resultCode + // error: { + // googlePayError: {}, + // applePayError: {} + // } + }); + } else { + actions.resolve({ + action: result.action, + order: result.order, + resultCode: result.resultCode, + donationToken: result.donationToken + }); + } + } catch (error) { + console.error('## onAdditionalDetails - critical error', error); + actions.reject(); } }, onBalanceCheck: async (resolve, reject, data) => { diff --git a/packages/playground/src/pages/Dropin/session.js b/packages/playground/src/pages/Dropin/session.js index f786939bc1..912f3790ca 100644 --- a/packages/playground/src/pages/Dropin/session.js +++ b/packages/playground/src/pages/Dropin/session.js @@ -28,7 +28,7 @@ export async function initSession() { actions.resolve(data); }, onPaymentCompleted: (result, component) => { - console.info(result, component); + console.info('onPaymentCompleted', result, component); }, onPaymentFailed(result, element) { console.log('onPaymentFailed', result, element); From 689b362e781609cc3ee0be8195f29914d6caede5 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 21 Dec 2023 09:57:05 -0300 Subject: [PATCH 25/55] checkout.submitDetails fix --- .../lib/src/components/ApplePay/ApplePay.tsx | 4 +- .../src/components/GooglePay/GooglePay.tsx | 4 +- .../internal/UIElement/UIElement.tsx | 26 ++------- .../components/internal/UIElement/utils.ts | 21 +++++++ packages/lib/src/core/core.ts | 57 +++++++++++++------ packages/lib/src/core/types.ts | 2 +- .../playground/src/pages/Dropin/manual.js | 25 ++------ .../playground/src/pages/Dropin/session.js | 4 +- .../playground/src/pages/Result/Result.js | 4 ++ 9 files changed, 83 insertions(+), 64 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index a89befe775..9c7a75156a 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -18,7 +18,7 @@ import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; import { onSubmitReject } from '../../core/types'; import { PaymentResponseData } from '../../types/global-types'; -import { sanitizeResponse } from '../internal/UIElement/utils'; +import { sanitizeResponse, verifyPaymentDidNotFail } from '../internal/UIElement/utils'; const latestSupportedVersion = 14; @@ -115,7 +115,7 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { this.handleAuthorization() .then(this.makePaymentsCall) .then(sanitizeResponse) - .then(this.verifyPaymentDidNotFail) + .then(verifyPaymentDidNotFail) .then(this.collectOrderTrackingDetailsIfNeeded) .then(({ paymentResponse, orderDetails }) => { resolve({ diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 5bad204efb..b77b696593 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -10,7 +10,7 @@ import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; import { onSubmitReject } from '../../core/types'; import { AddressData, PaymentResponseData } from '../../types/global-types'; -import { sanitizeResponse } from '../internal/UIElement/utils'; +import { sanitizeResponse, verifyPaymentDidNotFail } from '../internal/UIElement/utils'; class GooglePay extends UIElement<GooglePayConfiguration> { public static type = TxVariants.googlepay; @@ -110,7 +110,7 @@ class GooglePay extends UIElement<GooglePayConfiguration> { this.handleAuthorization() .then(this.makePaymentsCall) .then(sanitizeResponse) - .then(this.verifyPaymentDidNotFail) + .then(verifyPaymentDidNotFail) .then((paymentResponse: PaymentResponseData) => { resolve({ transactionState: 'SUCCESS' }); return paymentResponse; diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index f919b9aec1..5efcc788bf 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -1,7 +1,7 @@ import { h } from 'preact'; import BaseElement from '../BaseElement/BaseElement'; import PayButton from '../PayButton'; -import { sanitizeResponse } from './utils'; +import { cleanupFinalResult, sanitizeResponse, verifyPaymentDidNotFail } from './utils'; import AdyenCheckoutError from '../../../core/Errors/AdyenCheckoutError'; import { hasOwnProperty } from '../../../utils/hasOwnProperty'; import { CoreConfiguration, ICore } from '../../../core/types'; @@ -134,7 +134,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> this.makePaymentsCall() .then(sanitizeResponse) - .then(this.verifyPaymentDidNotFail) + .then(verifyPaymentDidNotFail) .then(this.handleResponse) .catch(this.handleFailedResult); } @@ -164,7 +164,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> this.handleError( new AdyenCheckoutError( 'IMPLEMENTATION_ERROR', - 'Could not perform /payments call. Callback "onSubmit" is missing or Checkout session is not available' + 'It can not perform /payments call. Callback "onSubmit" is missing or Checkout session is not available' ) ); } @@ -200,14 +200,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> // }; } - protected verifyPaymentDidNotFail(response: PaymentResponseData): Promise<PaymentResponseData> { - if (['Cancelled', 'Error', 'Refused'].includes(response.resultCode)) { - return Promise.reject(response); - } - - return Promise.resolve(response); - } - private onValid() { const state = { data: this.data }; if (this.props.onValid) this.props.onValid(state, this.elementRef); @@ -234,7 +226,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> protected handleAdditionalDetails(state: any): void { this.makeAdditionalDetailsCall(state) .then(sanitizeResponse) - .then(this.verifyPaymentDidNotFail) + .then(verifyPaymentDidNotFail) .then(this.handleResponse) .catch(this.handleFailedResult); } @@ -259,7 +251,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> this.handleError( new AdyenCheckoutError( 'IMPLEMENTATION_ERROR', - 'Could not perform /payments/details call. Callback "onAdditionalDetails" is missing or Checkout session is not available' + 'It can not perform /payments/details call. Callback "onAdditionalDetails" is missing or Checkout session is not available' ) ); } @@ -328,17 +320,11 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> }; protected handleSuccessResult = (result: PaymentResponseData): void => { - const sanitizeResult = (result: PaymentResponseData) => { - delete result.order; - delete result.action; - if (!result.donationToken || result.donationToken.length === 0) delete result.donationToken; - }; - if (this.props.setStatusAutomatically) { this.setElementStatus('success'); } - sanitizeResult(result); + cleanupFinalResult(result); this.props.onPaymentCompleted?.(result, this.elementRef); }; diff --git a/packages/lib/src/components/internal/UIElement/utils.ts b/packages/lib/src/components/internal/UIElement/utils.ts index 7f57ffab5a..eb3d030570 100644 --- a/packages/lib/src/components/internal/UIElement/utils.ts +++ b/packages/lib/src/components/internal/UIElement/utils.ts @@ -21,6 +21,19 @@ export function sanitizeResponse(response: RawPaymentResponse): PaymentResponseD return sanitizedObject as PaymentResponseData; } +/** + * Remove not relevant properties in the final payment result object + * + * @param paymentResponse + */ +export function cleanupFinalResult(paymentResponse: PaymentResponseData): void { + delete paymentResponse.order; + delete paymentResponse.action; + if (!paymentResponse.donationToken || paymentResponse.donationToken.length === 0) { + delete paymentResponse.donationToken; + } +} + export function resolveFinalResult(result: PaymentResponseData): [status: UIElementStatus, statusProps?: any] { switch (result.resultCode) { case 'Authorised': @@ -35,3 +48,11 @@ export function resolveFinalResult(result: PaymentResponseData): [status: UIElem default: } } + +export function verifyPaymentDidNotFail(response: PaymentResponseData): Promise<PaymentResponseData> { + if (['Cancelled', 'Error', 'Refused'].includes(response.resultCode)) { + return Promise.reject(response); + } + + return Promise.resolve(response); +} diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index b3542fc246..c8d0de5a3b 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -5,7 +5,7 @@ import PaymentMethods from './ProcessResponse/PaymentMethods'; import getComponentForAction from './ProcessResponse/PaymentAction'; import { resolveEnvironment, resolveCDNEnvironment } from './Environment'; import Analytics from './Analytics'; -import { PaymentAction } from '../types/global-types'; +import { OnPaymentFailedData, PaymentAction, PaymentResponseData } from '../types/global-types'; import { CoreConfiguration, ICore } from './types'; import { processGlobalOptions } from './utils'; import Session from './CheckoutSession'; @@ -14,6 +14,8 @@ import { Resources } from './Context/Resources'; import { SRPanel } from './Errors/SRPanel'; import registry, { NewableComponent } from './core.registry'; import { DEFAULT_LOCALE } from '../language/config'; +import { cleanupFinalResult, sanitizeResponse, verifyPaymentDidNotFail } from '../components/internal/UIElement/utils'; +import AdyenCheckoutError from './Errors/AdyenCheckoutError'; class Core implements ICore { public session?: Session; @@ -106,25 +108,48 @@ class Core implements ICore { } /** - * Submits details using onAdditionalDetails or the session flow if available - * @param details - + * Method used when handling redirects. It submits details using 'onAdditionalDetails' or the Sessions flow if available. + * + * @public + * @see {https://docs.adyen.com/online-payments/build-your-integration/?platform=Web&integration=Components&version=5.55.1#handle-the-redirect} + * @param details - Details object containing the redirectResult */ - public submitDetails(details): void { - // TODO: Check this - // if (this.options.onAdditionalDetails) { - // return this.options.onAdditionalDetails(details); - // } + public submitDetails(details: { details: { redirectResult: string } }): void { + let promise = null; + + if (this.options.onAdditionalDetails) { + promise = new Promise((resolve, reject) => { + this.options.onAdditionalDetails({ data: details }, undefined, { resolve, reject }); + }); + } if (this.session) { - this.session - .submitDetails(details) - .then(response => { - this.options.onPaymentCompleted?.(response); - }) - .catch(error => { - this.options.onError?.(error); - }); + promise = this.session.submitDetails(details).catch(error => { + this.options.onError?.(error); + return Promise.reject(error); + }); } + + if (!promise) { + this.options.onError?.( + new AdyenCheckoutError( + 'IMPLEMENTATION_ERROR', + 'It can not submit the details. The callback "onAdditionalDetails" or the Session is not setup correctly.' + ) + ); + return; + } + + promise + .then(sanitizeResponse) + .then(verifyPaymentDidNotFail) + .then((response: PaymentResponseData) => { + cleanupFinalResult(response); + this.options.onPaymentCompleted?.(response); + }) + .catch((result: OnPaymentFailedData) => { + this.options.onPaymentFailed?.(result); + }); } /** diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 2fc0c74f55..945c4daa64 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -208,7 +208,7 @@ export interface CoreConfiguration { * Callback used in the Advanced flow to perform the /payments/details API call. * * @param state - * @param element + * @param element - Component submitting details. It is undefined when using checkout.submitDetails() * @param actions */ onAdditionalDetails?( diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index baca6af945..c21c2c4304 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -1,6 +1,7 @@ import { AdyenCheckout, Dropin, + Ideal, Card, GooglePay, PayPal, @@ -137,8 +138,6 @@ export async function initManual() { }); function handleFinalState(resultCode, dropin) { - localStorage.removeItem('storedPaymentData'); - if (resultCode === 'Authorised' || resultCode === 'Received') { dropin.setStatus('success'); } else { @@ -147,26 +146,10 @@ export async function initManual() { } function handleRedirectResult() { - const storedPaymentData = localStorage.getItem('storedPaymentData'); const { amazonCheckoutSessionId, redirectResult, payload } = getSearchParameters(window.location.search); - if (redirectResult || payload) { - dropin.setStatus('loading'); - return makeDetailsCall({ - ...(storedPaymentData && { paymentData: storedPaymentData }), - details: { - ...(redirectResult && { redirectResult }), - ...(payload && { payload }) - } - }).then(result => { - if (result.action) { - dropin.handleAction(result.action); - } else { - handleFinalState(result.resultCode, dropin); - } - - return true; - }); + if (redirectResult) { + window.checkout.submitDetails({ details: { redirectResult } }); } // Handle Amazon Pay redirect result @@ -204,7 +187,7 @@ export async function initManual() { const dropin = new Dropin({ core: checkout, - paymentMethodComponents: [Card, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay], + paymentMethodComponents: [Card, Ideal, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay], instantPaymentTypes: ['googlepay'], paymentMethodsConfiguration: { card: { diff --git a/packages/playground/src/pages/Dropin/session.js b/packages/playground/src/pages/Dropin/session.js index 912f3790ca..b71f10cb16 100644 --- a/packages/playground/src/pages/Dropin/session.js +++ b/packages/playground/src/pages/Dropin/session.js @@ -1,4 +1,4 @@ -import { AdyenCheckout, Dropin, Card, WeChat, Giftcard, PayPal, Ach, GooglePay } from '@adyen/adyen-web'; +import { AdyenCheckout, Dropin, Card, WeChat, Giftcard, PayPal, Ach, GooglePay, Ideal } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; import { createSession } from '../../services'; import { amount, shopperLocale, shopperReference, countryCode, returnUrl } from '../../config/commonConfig'; @@ -44,7 +44,7 @@ export async function initSession() { const dropin = new Dropin({ core: checkout, instantPaymentTypes: ['googlepay'], - paymentMethodComponents: [Card, WeChat, Giftcard, PayPal, Ach, GooglePay], + paymentMethodComponents: [Card, WeChat, Giftcard, PayPal, Ach, GooglePay, Ideal], paymentMethodsConfiguration: { googlepay: { buttonType: 'plain', diff --git a/packages/playground/src/pages/Result/Result.js b/packages/playground/src/pages/Result/Result.js index f65cc00c5b..52001c524e 100644 --- a/packages/playground/src/pages/Result/Result.js +++ b/packages/playground/src/pages/Result/Result.js @@ -14,6 +14,10 @@ async function handleRedirectResult(redirectResult, sessionId) { console.log('onPaymentCompleted', result); document.querySelector('#result-container > pre').innerHTML = JSON.stringify(result, null, '\t'); }, + onPaymentFailed: result => { + console.log('onPaymentFailed', result); + document.querySelector('#result-container > pre').innerHTML = JSON.stringify(result, null, '\t'); + }, onError: obj => { console.log('checkout level merchant defined onError handler obj=', obj); } From 5879bc3b8d27ac83a220f15a79cc5f6cf1a009f1 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 21 Dec 2023 13:26:02 -0300 Subject: [PATCH 26/55] always resolving --- .../internal/UIElement/UIElement.tsx | 5 +- packages/lib/src/core/core.ts | 7 +- packages/lib/src/core/types.ts | 28 ++++---- packages/lib/src/types/global-types.ts | 9 +-- packages/playground/src/handlers.js | 55 ++++++++-------- .../playground/src/pages/Dropin/manual.js | 65 ++++++++----------- 6 files changed, 82 insertions(+), 87 deletions(-) diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 5efcc788bf..96b63cd0c9 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -9,7 +9,6 @@ import { Resources } from '../../../core/Context/Resources'; import { NewableComponent } from '../../../core/core.registry'; import { ComponentMethodsRef, IUIElement, PayButtonFunctionProps, UIElementProps, UIElementStatus } from './types'; import { - OnPaymentFailedData, Order, PaymentAction, PaymentData, @@ -311,11 +310,13 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> * * @param result */ - protected handleFailedResult = (result: OnPaymentFailedData): void => { + protected handleFailedResult = (result: PaymentResponseData): void => { if (this.props.setStatusAutomatically) { this.setElementStatus('error'); } + cleanupFinalResult(result); + this.props.onPaymentFailed?.(result, this.elementRef); }; diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index c8d0de5a3b..ca4cbf1e6b 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -5,7 +5,7 @@ import PaymentMethods from './ProcessResponse/PaymentMethods'; import getComponentForAction from './ProcessResponse/PaymentAction'; import { resolveEnvironment, resolveCDNEnvironment } from './Environment'; import Analytics from './Analytics'; -import { OnPaymentFailedData, PaymentAction, PaymentResponseData } from '../types/global-types'; +import { PaymentAction, PaymentResponseData } from '../types/global-types'; import { CoreConfiguration, ICore } from './types'; import { processGlobalOptions } from './utils'; import Session from './CheckoutSession'; @@ -147,8 +147,9 @@ class Core implements ICore { cleanupFinalResult(response); this.options.onPaymentCompleted?.(response); }) - .catch((result: OnPaymentFailedData) => { - this.options.onPaymentFailed?.(result); + .catch((response: PaymentResponseData) => { + cleanupFinalResult(response); + this.options.onPaymentFailed?.(response); }); } diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 945c4daa64..190e6cdbd6 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -5,11 +5,11 @@ import { PaymentAction, PaymentMethodsResponse, ActionHandledReturnObject, - OnPaymentCompletedData, PaymentData, PaymentResponseAdvancedFlow, - OnPaymentFailedData, - PaymentMethodsRequestData + PaymentMethodsRequestData, + SessionsResponse, + ResultCode } from '../types/global-types'; import { AnalyticsOptions } from './Analytics/types'; import { RiskModuleOptions } from './RiskModule/RiskModule'; @@ -51,12 +51,12 @@ export interface ICore { export type AdyenEnvironment = 'test' | 'live' | 'live-us' | 'live-au' | 'live-apse' | 'live-in' | string; -export type onSubmitReject = { - error?: { - googlePayError?: Partial<google.payments.api.PaymentDataError>; - applePayError?: ApplePayJS.ApplePayError[] | ApplePayJS.ApplePayError; - }; -}; +// export type onSubmitReject = { +// error?: { +// googlePayError?: Partial<google.payments.api.PaymentDataError>; +// applePayError?: ApplePayJS.ApplePayError[] | ApplePayJS.ApplePayError; +// }; +// }; export interface CoreConfiguration { session?: any; @@ -182,7 +182,7 @@ export interface CoreConfiguration { * @param data * @param element */ - onPaymentCompleted?(data: OnPaymentCompletedData, element?: UIElement): void; + onPaymentCompleted?(data: SessionsResponse | { resultCode: ResultCode }, element?: UIElement): void; /** * Called when the payment fails. @@ -193,14 +193,15 @@ export interface CoreConfiguration { * @param data * @param element */ - onPaymentFailed?(data?: OnPaymentFailedData, element?: UIElement): void; + onPaymentFailed?(data?: SessionsResponse | { resultCode: ResultCode }, element?: UIElement): void; onSubmit?( state: any, element: UIElement, actions: { resolve: (response: PaymentResponseAdvancedFlow) => void; - reject: (error?: onSubmitReject) => void; + reject: () => void; + // reject: (error?: onSubmitReject) => void; } ): void; @@ -216,7 +217,8 @@ export interface CoreConfiguration { element: UIElement, actions: { resolve: (response: PaymentResponseAdvancedFlow) => void; - reject: (error?: onSubmitReject) => void; + reject: () => void; + // reject: (error?: onSubmitReject) => void; } ): void; diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index e78fc5699f..2619a51dfa 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -1,6 +1,6 @@ import { ADDRESS_SCHEMA } from '../components/internal/Address/constants'; import actionTypes from '../core/ProcessResponse/PaymentAction/actionTypes'; -import { onSubmitReject } from '../core/types'; +// import { onSubmitReject } from '../core/types'; export type PaymentActionsType = keyof typeof actionTypes; @@ -335,9 +335,6 @@ export type SessionsResponse = { sessionResult: string; resultCode: ResultCode; }; -export type OnPaymentCompletedData = SessionsResponse | { resultCode: ResultCode }; - -export type OnPaymentFailedData = SessionsResponse | onSubmitReject; //TODO double check these values export interface PaymentMethodsRequestData { @@ -351,6 +348,10 @@ export interface PaymentResponseAdvancedFlow { action?: PaymentAction; order?: Order; donationToken?: string; + error?: { + googlePayError?: Partial<google.payments.api.PaymentDataError>; + applePayError?: ApplePayJS.ApplePayError[] | ApplePayJS.ApplePayError; + }; } export interface PaymentResponseData { diff --git a/packages/playground/src/handlers.js b/packages/playground/src/handlers.js index ccaae50d34..487fee13dc 100644 --- a/packages/playground/src/handlers.js +++ b/packages/playground/src/handlers.js @@ -32,42 +32,43 @@ export function handleError(obj) { export async function handleSubmit(state, component, actions) { component.setStatus('loading'); + console.log('onSubmit', state, actions); + try { - const result = await makePayment(state.data); + const { action, order, resultCode, donationToken } = await makePayment(state.data); - if (!result.resultCode) actions.reject(); + if (!resultCode) actions.reject(); - if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { - actions.reject({ - resultCode: result.resultCode, - error: { - googlePayError: {}, - applePayError: {} - } - }); - } else { - actions.resolve({ - action: result.action, - order: result.order, - resultCode: result.resultCode, - donationToken: result.donationToken - }); - } + actions.resolve({ + resultCode, + action, + order, + donationToken + }); } catch (error) { console.error('## onSubmit - critical error', error); actions.reject(); } } -export function handleAdditionalDetails(details, component) { - // component.setStatus('processing'); +export async function handleAdditionalDetails(state, component, actions) { + try { + console.log('onAdditionalDetails', state, component, actions); + + const { resultCode, action, order, resultCode, donationToken } = await makeDetailsCall(state.data); - return makeDetailsCall(details.data) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + // error: {}, }); + return; + } catch (error) { + console.error('## onAdditionalDetails - critical error', error); + actions.reject(); + } } diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index c21c2c4304..aaa4e81c0f 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -42,26 +42,21 @@ export async function initManual() { console.log('onSubmit', state, component.authorizedEvent); try { - const result = await makePayment(state.data); - - if (!result.resultCode) actions.reject(); - - if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { - actions.reject({ - resultCode: result.resultCode - // error: { - // googlePayError: {}, - // applePayError: {} - // } - }); - } else { - actions.resolve({ - action: result.action, - order: result.order, - resultCode: result.resultCode, - donationToken: result.donationToken - }); - } + const { action, order, resultCode, donationToken } = await makePayment(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + // error: { + // googlePayError: {}, + // applePayError: {} + // } + // } + }); } catch (error) { console.error('## onSubmit - critical error', error); actions.reject(); @@ -81,26 +76,20 @@ export async function initManual() { onAdditionalDetails: async (state, component, actions) => { try { - const result = await makeDetailsCall(state.data); + console.log('onAdditionalDetails', state, component, actions); - if (!result.resultCode) actions.reject(); + const { resultCode, action, order, resultCode, donationToken } = await makeDetailsCall(state.data); - if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { - actions.reject({ - resultCode: result.resultCode - // error: { - // googlePayError: {}, - // applePayError: {} - // } - }); - } else { - actions.resolve({ - action: result.action, - order: result.order, - resultCode: result.resultCode, - donationToken: result.donationToken - }); - } + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + // error: {}, + }); + return; } catch (error) { console.error('## onAdditionalDetails - critical error', error); actions.reject(); From ab0e7ba5e99ad6161228dfcf78dbd5f63d3d6fa4 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 21 Dec 2023 14:41:33 -0300 Subject: [PATCH 27/55] rejecting googlepay onauthorize with error --- .../src/components/GooglePay/GooglePay.tsx | 32 +++++++++++++------ .../lib/src/components/GooglePay/types.ts | 7 ++-- .../internal/UIElement/UIElement.tsx | 12 +++---- .../components/internal/UIElement/utils.ts | 2 +- packages/lib/src/core/types.ts | 16 +++++----- packages/lib/src/types/global-types.ts | 13 ++++---- .../playground/src/pages/Wallets/Wallets.js | 13 ++++++-- 7 files changed, 58 insertions(+), 37 deletions(-) diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index b77b696593..66d94a45c5 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -8,8 +8,7 @@ import { formatGooglePayContactToAdyenAddressFormat, getGooglePayLocale } from ' import collectBrowserInfo from '../../utils/browserInfo'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; -import { onSubmitReject } from '../../core/types'; -import { AddressData, PaymentResponseData } from '../../types/global-types'; +import { AddressData, PaymentResponseData, RawPaymentResponse } from '../../types/global-types'; import { sanitizeResponse, verifyPaymentDidNotFail } from '../internal/UIElement/utils'; class GooglePay extends UIElement<GooglePayConfiguration> { @@ -118,19 +117,30 @@ class GooglePay extends UIElement<GooglePayConfiguration> { .then(paymentResponse => { this.handleResponse(paymentResponse); }) - .catch((error: onSubmitReject) => { + .catch((paymentResponse: RawPaymentResponse) => { this.setElementStatus('ready'); + const googlePayError = paymentResponse.error?.googlePayError; + + const error: google.payments.api.PaymentDataError = + typeof googlePayError === 'string' || undefined + ? { + intent: 'PAYMENT_AUTHORIZATION', + reason: 'OTHER_ERROR', + message: (googlePayError as string) || 'Payment failed' + } + : { + intent: googlePayError?.intent || 'PAYMENT_AUTHORIZATION', + reason: googlePayError?.reason || 'OTHER_ERROR', + message: googlePayError?.message || 'Payment failed' + }; + resolve({ transactionState: 'ERROR', - error: { - intent: error?.error?.googlePayError?.intent || 'PAYMENT_AUTHORIZATION', - message: error?.error?.googlePayError?.message || 'Payment failed', - reason: error?.error?.googlePayError?.reason || 'OTHER_ERROR' - } + error }); - this.handleFailedResult(error); + this.handleFailedResult(paymentResponse); }); }); }; @@ -155,6 +165,10 @@ class GooglePay extends UIElement<GooglePayConfiguration> { }, { resolve, reject } ); + }).catch((error?: google.payments.api.PaymentDataError | string) => { + // Format error in a way that the 'catch' of the 'onPaymentAuthorize' block accepts it + const data = { error: { googlePayError: error } }; + return Promise.reject(data); }); } diff --git a/packages/lib/src/components/GooglePay/types.ts b/packages/lib/src/components/GooglePay/types.ts index ada06be4c4..33d8be26cb 100644 --- a/packages/lib/src/components/GooglePay/types.ts +++ b/packages/lib/src/components/GooglePay/types.ts @@ -152,11 +152,8 @@ export interface GooglePayConfiguration extends UIElementProps { onClick?: (resolve, reject) => void; /** - * Callback called when GooglePay authorize the payment. + * Callback called when GooglePay authorizes the payment. * Must be resolved/rejected with the action object. - * - * @param paymentData - * @returns */ onAuthorized?: ( data: { @@ -164,7 +161,7 @@ export interface GooglePayConfiguration extends UIElementProps { billingAddress?: Partial<AddressData>; deliveryAddress?: Partial<AddressData>; }, - actions: { resolve: () => void; reject: () => void } + actions: { resolve: () => void; reject: (error?: google.payments.api.PaymentDataError | string) => void } ) => void; } diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 96b63cd0c9..78b642d837 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -13,7 +13,7 @@ import { PaymentAction, PaymentData, PaymentMethodsResponse, - PaymentResponseAdvancedFlow, + CheckoutAdvancedFlowResponse, PaymentResponseData, RawPaymentResponse } from '../../../types/global-types'; @@ -138,7 +138,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> .catch(this.handleFailedResult); } - protected makePaymentsCall(): Promise<PaymentResponseAdvancedFlow | CheckoutSessionPaymentResponse> { + protected makePaymentsCall(): Promise<CheckoutAdvancedFlowResponse | CheckoutSessionPaymentResponse> { if (this.props.setStatusAutomatically) { this.setElementStatus('loading'); } @@ -168,8 +168,8 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> ); } - private async submitUsingAdvancedFlow(): Promise<PaymentResponseAdvancedFlow> { - return new Promise<PaymentResponseAdvancedFlow>((resolve, reject) => { + private async submitUsingAdvancedFlow(): Promise<CheckoutAdvancedFlowResponse> { + return new Promise<CheckoutAdvancedFlowResponse>((resolve, reject) => { this.props.onSubmit( { data: this.data, @@ -232,13 +232,13 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> private makeAdditionalDetailsCall( state: any - ): Promise<CheckoutSessionDetailsResponse | PaymentResponseAdvancedFlow> { + ): Promise<CheckoutSessionDetailsResponse | CheckoutAdvancedFlowResponse> { if (this.props.setStatusAutomatically) { this.setElementStatus('loading'); } if (this.props.onAdditionalDetails) { - return new Promise<PaymentResponseAdvancedFlow>((resolve, reject) => { + return new Promise<CheckoutAdvancedFlowResponse>((resolve, reject) => { this.props.onAdditionalDetails(state, this.elementRef, { resolve, reject }); }); } diff --git a/packages/lib/src/components/internal/UIElement/utils.ts b/packages/lib/src/components/internal/UIElement/utils.ts index eb3d030570..2466e68767 100644 --- a/packages/lib/src/components/internal/UIElement/utils.ts +++ b/packages/lib/src/components/internal/UIElement/utils.ts @@ -1,7 +1,7 @@ import { UIElementStatus } from './types'; import { RawPaymentResponse, PaymentResponseData } from '../../../types/global-types'; -const ALLOWED_PROPERTIES = ['action', 'resultCode', 'sessionData', 'order', 'sessionResult', 'donationToken']; +const ALLOWED_PROPERTIES = ['action', 'resultCode', 'sessionData', 'order', 'sessionResult', 'donationToken', 'error']; export function sanitizeResponse(response: RawPaymentResponse): PaymentResponseData { const removedProperties = []; diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 190e6cdbd6..05066ff8d8 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -6,7 +6,7 @@ import { PaymentMethodsResponse, ActionHandledReturnObject, PaymentData, - PaymentResponseAdvancedFlow, + CheckoutAdvancedFlowResponse, PaymentMethodsRequestData, SessionsResponse, ResultCode @@ -156,23 +156,23 @@ export interface CoreConfiguration { setStatusAutomatically?: boolean; beforeRedirect?( - resolve: PromiseResolve, + resolve: () => void, reject: PromiseReject, redirectData: { url: string; method: string; data?: any; } - ): Promise<void>; + ): void; beforeSubmit?( state: any, element: UIElement, actions: { - resolve: PromiseResolve; - reject: PromiseReject; + resolve: (data: any) => void; + reject: () => void; } - ): Promise<void>; + ): void; /** * Called when the payment succeeds. @@ -199,7 +199,7 @@ export interface CoreConfiguration { state: any, element: UIElement, actions: { - resolve: (response: PaymentResponseAdvancedFlow) => void; + resolve: (response: CheckoutAdvancedFlowResponse) => void; reject: () => void; // reject: (error?: onSubmitReject) => void; } @@ -216,7 +216,7 @@ export interface CoreConfiguration { state: any, element: UIElement, actions: { - resolve: (response: PaymentResponseAdvancedFlow) => void; + resolve: (response: CheckoutAdvancedFlowResponse) => void; reject: () => void; // reject: (error?: onSubmitReject) => void; } diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index 2619a51dfa..09ac0cd8e4 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -343,30 +343,31 @@ export interface PaymentMethodsRequestData { countryCode?: string; } -export interface PaymentResponseAdvancedFlow { +export interface CheckoutAdvancedFlowResponse { resultCode: ResultCode; action?: PaymentAction; order?: Order; donationToken?: string; error?: { - googlePayError?: Partial<google.payments.api.PaymentDataError>; + googlePayError?: google.payments.api.PaymentDataError | string; applePayError?: ApplePayJS.ApplePayError[] | ApplePayJS.ApplePayError; }; } export interface PaymentResponseData { + resultCode: ResultCode; type?: string; action?: PaymentAction; - resultCode: ResultCode; sessionData?: string; sessionResult?: string; order?: Order; donationToken?: string; } -export interface RawPaymentResponse extends PaymentResponseData { - [key: string]: any; -} +export type RawPaymentResponse = PaymentResponseData & + CheckoutAdvancedFlowResponse & { + [key: string]: any; + }; export type ActionDescriptionType = | 'qr-code-loaded' diff --git a/packages/playground/src/pages/Wallets/Wallets.js b/packages/playground/src/pages/Wallets/Wallets.js index d912669053..a227c7fa94 100644 --- a/packages/playground/src/pages/Wallets/Wallets.js +++ b/packages/playground/src/pages/Wallets/Wallets.js @@ -167,8 +167,17 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = // Callbacks onAuthorized(data, actions) { - console.log(data, actions); - actions.resolve(); + console.log('onAuthorized', data, actions); + + actions.reject(); + + // actions.reject('Failed with string'); + + // actions.reject({ + // intent: 'PAYMENT_AUTHORIZATION', + // reason: 'OTHER_ERROR', + // message: 'Failed with object' + // }); }, // onError: console.error, From 3c930e9deb41683ba1a21ed0256196c60ff0273d Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Fri, 22 Dec 2023 10:02:53 -0300 Subject: [PATCH 28/55] applepay rejecting onauthorized --- .../lib/src/components/ApplePay/ApplePay.tsx | 14 ++++++----- packages/lib/src/components/ApplePay/types.ts | 10 +++++--- packages/lib/src/components/Card/Card.tsx | 22 +++++++++++++----- packages/lib/src/components/Card/types.ts | 1 + .../src/components/CustomCard/CustomCard.tsx | 4 +++- .../components/ThreeDS2/ThreeDS2Challenge.tsx | 6 ++++- .../ThreeDS2/ThreeDS2DeviceFingerprint.tsx | 8 ++++++- .../internal/UIElement/UIElement.tsx | 3 --- .../components/internal/UIElement/types.ts | 17 +++++--------- packages/lib/src/core/config.ts | 1 - packages/lib/src/core/core.registry.ts | 1 + packages/lib/src/core/types.ts | 23 +++---------------- 12 files changed, 57 insertions(+), 53 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index 9c7a75156a..d949d221a8 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -16,8 +16,7 @@ import { } from './types'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; -import { onSubmitReject } from '../../core/types'; -import { PaymentResponseData } from '../../types/global-types'; +import { PaymentResponseData, RawPaymentResponse } from '../../types/global-types'; import { sanitizeResponse, verifyPaymentDidNotFail } from '../internal/UIElement/utils'; const latestSupportedVersion = 14; @@ -127,16 +126,15 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { .then(paymentResponse => { this.handleResponse(paymentResponse); }) - .catch((error: onSubmitReject) => { - console.error(error); - const errors = error?.error?.applePayError; + .catch((paymentResponse: RawPaymentResponse) => { + const errors = paymentResponse?.error?.applePayError; reject({ status: ApplePaySession.STATUS_FAILURE, errors: errors ? (Array.isArray(errors) ? errors : [errors]) : undefined }); - this.handleFailedResult(error); + this.handleFailedResult(paymentResponse); }); } }); @@ -172,6 +170,10 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { }, { resolve, reject } ); + }).catch((error?: ApplePayJS.ApplePayError) => { + // Format error in a way that the 'catch' of the 'onPaymentAuthorize' block accepts it + const data = { error: { applePayError: error } }; + return Promise.reject(data); }); } diff --git a/packages/lib/src/components/ApplePay/types.ts b/packages/lib/src/components/ApplePay/types.ts index 7ce2bfdd4a..b443b4b704 100644 --- a/packages/lib/src/components/ApplePay/types.ts +++ b/packages/lib/src/components/ApplePay/types.ts @@ -153,8 +153,12 @@ export interface ApplePayConfiguration extends UIElementProps { * Callback called when ApplePay authorize the payment. * Must be resolved/rejected with the action object. * - * @param paymentData - * @returns + * @param data - Authorization event from ApplePay, along with formatted billingAddress and deliveryAddress + * @param actions - Object to continue/stop with the payment flow + * + * @remarks + * If actions.resolve() is called, the payment flow will be triggered. + * If actions.reject() is called, the overlay will display an error */ onAuthorized?: ( data: { @@ -162,7 +166,7 @@ export interface ApplePayConfiguration extends UIElementProps { billingAddress?: Partial<AddressData>; deliveryAddress?: Partial<AddressData>; }, - actions: { resolve: () => void; reject: () => void } + actions: { resolve: () => void; reject: (error?: ApplePayJS.ApplePayError) => void } ) => void; /** diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index cf8064da37..c425ccbc89 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -15,6 +15,7 @@ import SRPanelProvider from '../../core/Errors/SRPanelProvider'; import { TxVariants } from '../tx-variants'; import { UIElementStatus } from '../internal/UIElement/types'; +// @ts-ignore TODO: Check with nick export class CardElement extends UIElement<CardConfiguration> { public static type = TxVariants.scheme; @@ -29,7 +30,11 @@ export class CardElement extends UIElement<CardConfiguration> { super(props); if (props && !props._disableClickToPay) { - this.clickToPayService = createClickToPayService(this.props.configuration, this.props.clickToPayConfiguration, this.props.environment); + this.clickToPayService = createClickToPayService( + this.props.configuration, + this.props.clickToPayConfiguration, + this.props.environment + ); this.clickToPayService?.initialize(); } } @@ -85,8 +90,10 @@ export class CardElement extends UIElement<CardConfiguration> { clickToPayConfiguration: { ...props.clickToPayConfiguration, disableOtpAutoFocus: props.clickToPayConfiguration?.disableOtpAutoFocus || false, - shopperEmail: props.clickToPayConfiguration?.shopperEmail || props?.core?.options?.session?.shopperEmail, - telephoneNumber: props.clickToPayConfiguration?.telephoneNumber || props?.core?.options?.session?.telephoneNumber, + shopperEmail: + props.clickToPayConfiguration?.shopperEmail || props?.core?.options?.session?.shopperEmail, + telephoneNumber: + props.clickToPayConfiguration?.telephoneNumber || props?.core?.options?.session?.telephoneNumber, locale: props.clickToPayConfiguration?.locale || props.i18n?.locale?.replace('-', '_') } }; @@ -103,7 +110,8 @@ export class CardElement extends UIElement<CardConfiguration> { * - or, in the case of a storedCard */ const cardBrand = this.state.selectedBrandValue; - const includeStorePaymentMethod = this.props.enableStoreDetails && typeof this.state.storePaymentMethod !== 'undefined'; + const includeStorePaymentMethod = + this.props.enableStoreDetails && typeof this.state.storePaymentMethod !== 'undefined'; return { paymentMethod: { @@ -137,7 +145,8 @@ export class CardElement extends UIElement<CardConfiguration> { }; processBinLookupResponse(binLookupResponse: BinLookupResponse, isReset = false) { - if (this.componentRef?.processBinLookupResponse) this.componentRef.processBinLookupResponse(binLookupResponse, isReset); + if (this.componentRef?.processBinLookupResponse) + this.componentRef.processBinLookupResponse(binLookupResponse, isReset); return this; } @@ -194,7 +203,8 @@ export class CardElement extends UIElement<CardConfiguration> { return ( (this.props.name || CardElement.type) + (this.props.storedPaymentMethodId - ? ' ' + this.props.i18n.get('creditCard.storedCard.description.ariaLabel').replace('%@', this.props.lastFour) + ? ' ' + + this.props.i18n.get('creditCard.storedCard.description.ariaLabel').replace('%@', this.props.lastFour) : '') ); } diff --git a/packages/lib/src/components/Card/types.ts b/packages/lib/src/components/Card/types.ts index a9d0d63523..7dd84e28a3 100644 --- a/packages/lib/src/components/Card/types.ts +++ b/packages/lib/src/components/Card/types.ts @@ -17,6 +17,7 @@ import { DisclaimerMsgObject } from '../internal/DisclaimerMessage/DisclaimerMes import { Placeholders } from './components/CardInput/types'; import { UIElementProps } from '../internal/UIElement/types'; +// @ts-ignore TODO: Check with nick export interface CardConfiguration extends UIElementProps { /** * Only set for a stored card, diff --git a/packages/lib/src/components/CustomCard/CustomCard.tsx b/packages/lib/src/components/CustomCard/CustomCard.tsx index c7c32d9c6f..14b25e4a5c 100644 --- a/packages/lib/src/components/CustomCard/CustomCard.tsx +++ b/packages/lib/src/components/CustomCard/CustomCard.tsx @@ -15,6 +15,7 @@ import { CustomCardConfiguration } from './types'; // type // countryCode +// @ts-ignore TODO: Check with nick export class CustomCard extends UIElement<CustomCardConfiguration> { public static type = TxVariants.customCard; @@ -80,7 +81,8 @@ export class CustomCard extends UIElement<CustomCardConfiguration> { if (!nuObj.isReset) { // Add brandImage urls, first checking if the merchant has configured their own one for the brand nuObj.supportedBrandsRaw = obj.supportedBrandsRaw?.map((item: BrandObject) => { - item.brandImageUrl = this.props.brandsConfiguration[item.brand]?.icon ?? getCardImageUrl(item.brand, this.resources); + item.brandImageUrl = + this.props.brandsConfiguration[item.brand]?.icon ?? getCardImageUrl(item.brand, this.resources); return item; }); } diff --git a/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx b/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx index a386f96a77..e16b956501 100644 --- a/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx +++ b/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx @@ -7,6 +7,7 @@ import { hasOwnProperty } from '../../utils/hasOwnProperty'; import { TxVariants } from '../tx-variants'; import { ThreeDS2ChallengeConfiguration } from './types'; +// @ts-ignore TODO: Check with nick class ThreeDS2Challenge extends UIElement<ThreeDS2ChallengeConfiguration> { public static type = TxVariants.threeDS2Challenge; @@ -30,7 +31,10 @@ class ThreeDS2Challenge extends UIElement<ThreeDS2ChallengeConfiguration> { */ const dataTypeForError = hasOwnProperty(this.props, 'isMDFlow') ? 'paymentData' : 'authorisationToken'; - this.props.onError({ errorCode: 'threeds2.challenge', message: `No ${dataTypeForError} received. Challenge cannot proceed` }); + this.props.onError({ + errorCode: 'threeds2.challenge', + message: `No ${dataTypeForError} received. Challenge cannot proceed` + }); return null; } diff --git a/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx b/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx index 5949051e9b..4503c2ee64 100644 --- a/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx +++ b/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx @@ -6,6 +6,7 @@ import { existy } from '../internal/SecuredFields/lib/utilities/commonUtils'; import { TxVariants } from '../tx-variants'; import { ThreeDS2DeviceFingerprintConfiguration } from './types'; +// @ts-ignore TODO: Check with nick class ThreeDS2DeviceFingerprint extends UIElement<ThreeDS2DeviceFingerprintConfiguration> { public static type = TxVariants.threeDS2Fingerprint; @@ -38,7 +39,12 @@ class ThreeDS2DeviceFingerprint extends UIElement<ThreeDS2DeviceFingerprintConfi * this.props.isMDFlow indicates a threeds2InMDFlow process. It means the action to create this component came from the threeds2InMDFlow process * and upon completion should call the passed onComplete callback (instead of the /submitThreeDS2Fingerprint endpoint for the regular, "native" flow) */ - return <PrepareFingerprint {...this.props} onComplete={this.props.isMDFlow ? this.onComplete : this.callSubmit3DS2Fingerprint} />; + return ( + <PrepareFingerprint + {...this.props} + onComplete={this.props.isMDFlow ? this.onComplete : this.callSubmit3DS2Fingerprint} + /> + ); } } diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 78b642d837..5dab328a73 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -344,9 +344,6 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> return; } - /** - * TODO: handle order properly on advanced flow. - */ if (response.order?.remainingAmount?.value > 0) { // we don't want to call elementRef here, use the component handler // we do this way so the logic on handlingOrder is associated with payment method diff --git a/packages/lib/src/components/internal/UIElement/types.ts b/packages/lib/src/components/internal/UIElement/types.ts index 2f57ef45fb..111e25d6d8 100644 --- a/packages/lib/src/components/internal/UIElement/types.ts +++ b/packages/lib/src/components/internal/UIElement/types.ts @@ -1,12 +1,7 @@ import { h } from 'preact'; import Session from '../../../core/CheckoutSession'; import UIElement from './UIElement'; -import { - ActionHandledReturnObject, - PaymentAction, - PaymentAmount, - PaymentAmountExtended -} from '../../../types/global-types'; +import { PaymentAction, PaymentAmount, PaymentAmountExtended } from '../../../types/global-types'; import Language from '../../../language'; import { BaseElementProps, IBaseElement } from '../BaseElement/types'; import { PayButtonProps } from '../PayButton/PayButton'; @@ -16,26 +11,26 @@ export type PayButtonFunctionProps = Omit<PayButtonProps, 'amount'>; type CoreCallbacks = Pick< CoreConfiguration, + | 'beforeRedirect' + | 'beforeSubmit' | 'onSubmit' | 'onAdditionalDetails' | 'onPaymentFailed' | 'onPaymentCompleted' | 'onOrderUpdated' | 'onPaymentMethodsRequest' + | 'onChange' + | 'onActionHandled' + | 'onError' >; export type UIElementProps = BaseElementProps & CoreCallbacks & { environment?: string; session?: Session; - onChange?: (state: any, element: UIElement) => void; onValid?: (state: any, element: UIElement) => void; - beforeSubmit?: (state: any, element: UIElement, actions: any) => Promise<void>; onComplete?: (state, element: UIElement) => void; - onActionHandled?: (rtnObj: ActionHandledReturnObject) => void; - onError?: (error, element?: UIElement) => void; - beforeRedirect?: (resolve, reject, redirectData, element: UIElement) => void; isInstantPayment?: boolean; diff --git a/packages/lib/src/core/config.ts b/packages/lib/src/core/config.ts index 4b2daf6564..ad415499c4 100644 --- a/packages/lib/src/core/config.ts +++ b/packages/lib/src/core/config.ts @@ -25,7 +25,6 @@ export const GENERIC_OPTIONS = [ 'onSubmit', 'onActionHandled', 'onAdditionalDetails', - 'onCancel', 'onChange', 'onError', 'onBalanceCheck', diff --git a/packages/lib/src/core/core.registry.ts b/packages/lib/src/core/core.registry.ts index 49481aa11d..35b421b1bb 100644 --- a/packages/lib/src/core/core.registry.ts +++ b/packages/lib/src/core/core.registry.ts @@ -23,6 +23,7 @@ const defaultComponents = { }; class Registry implements IRegistry { + // @ts-ignore TODO: Check with nick public componentsMap: Record<string, NewableComponent> = defaultComponents; public supportedTxVariants: Set<string> = new Set(Object.values(TxVariants)); diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 05066ff8d8..ab25bba2bf 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -21,10 +21,6 @@ import { NewableComponent } from './core.registry'; import Session from './CheckoutSession'; import PaymentMethods from './ProcessResponse/PaymentMethods'; -type PromiseResolve = typeof Promise.resolve; - -type PromiseReject = typeof Promise.reject; - export interface ICore { initialize(): Promise<ICore>; @@ -51,13 +47,6 @@ export interface ICore { export type AdyenEnvironment = 'test' | 'live' | 'live-us' | 'live-au' | 'live-apse' | 'live-in' | string; -// export type onSubmitReject = { -// error?: { -// googlePayError?: Partial<google.payments.api.PaymentDataError>; -// applePayError?: ApplePayJS.ApplePayError[] | ApplePayJS.ApplePayError; -// }; -// }; - export interface CoreConfiguration { session?: any; /** @@ -157,7 +146,7 @@ export interface CoreConfiguration { beforeRedirect?( resolve: () => void, - reject: PromiseReject, + reject: () => void, redirectData: { url: string; method: string; @@ -228,9 +217,9 @@ export interface CoreConfiguration { onError?(error: AdyenCheckoutError, element?: UIElement): void; - onBalanceCheck?(resolve: PromiseResolve, reject: PromiseReject, data: GiftCardElementData): Promise<void>; + onBalanceCheck?(resolve: () => void, reject: () => void, data: GiftCardElementData): Promise<void>; - onOrderRequest?(resolve: PromiseResolve, reject: PromiseReject, data: PaymentData): Promise<void>; + onOrderRequest?(resolve: () => void, reject: () => void, data: PaymentData): Promise<void>; onPaymentMethodsRequest?( data: PaymentMethodsRequestData, @@ -246,12 +235,6 @@ export interface CoreConfiguration { */ onOrderUpdated?(data: { order: Order }): void; - /** - * Used only in the Donation Component when shopper declines to donate - * https://docs.adyen.com/online-payments/donations/web-component - */ - onCancel?(): void; - /** * @internal */ From 638c0f8b6fb3cb5e6e6bbc30bb38ef4bf5de5ceb Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Tue, 16 Jan 2024 10:25:50 -0300 Subject: [PATCH 29/55] fix: current tests --- .../lib/src/components/Dropin/Dropin.test.ts | 26 ++++++++++++++----- .../internal/UIElement/UIElement.test.ts | 22 ++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/lib/src/components/Dropin/Dropin.test.ts b/packages/lib/src/components/Dropin/Dropin.test.ts index f01661740c..e66ba07684 100644 --- a/packages/lib/src/components/Dropin/Dropin.test.ts +++ b/packages/lib/src/components/Dropin/Dropin.test.ts @@ -63,7 +63,9 @@ describe('Dropin', () => { dropin.handleAction(fingerprintAction); expect(dropin.componentFromAction instanceof ThreeDS2DeviceFingerprint).toEqual(true); - expect((dropin.componentFromAction as ThreeDS2DeviceFingerprint).props.showSpinner).toEqual(false); + expect((dropin.componentFromAction as unknown as ThreeDS2DeviceFingerprint).props.showSpinner).toEqual( + false + ); expect(dropin.componentFromAction.props.statusType).toEqual('loading'); expect(dropin.componentFromAction.props.isDropin).toBe(true); }); @@ -83,7 +85,7 @@ describe('Dropin', () => { expect(dropin.componentFromAction instanceof ThreeDS2Challenge).toEqual(true); expect(dropin.componentFromAction.props.statusType).toEqual('custom'); expect(dropin.componentFromAction.props.isDropin).toBe(true); - expect((dropin.componentFromAction as ThreeDS2Challenge).props.size).toEqual('02'); + expect((dropin.componentFromAction as unknown as ThreeDS2Challenge).props.size).toEqual('02'); }); test('new challenge action gets challengeWindowSize from paymentMethodsConfiguration', async () => { @@ -101,12 +103,17 @@ describe('Dropin', () => { analytics: { enabled: false } }); - const dropin = new Dropin({ core: checkout, paymentMethodsConfiguration: { card: { challengeWindowSize: '02' } } }); + const dropin = new Dropin({ + core: checkout, + paymentMethodsConfiguration: { card: { challengeWindowSize: '02' } } + }); jest.spyOn(dropin, 'activePaymentMethod', 'get').mockReturnValue({ props: { challengeWindowSize: '02' } }); dropin.handleAction(challengeAction); expect(dropin.componentFromAction instanceof ThreeDS2Challenge).toEqual(true); - expect((dropin.componentFromAction as ThreeDS2Challenge).props.challengeWindowSize).toEqual('02'); + expect((dropin.componentFromAction as unknown as ThreeDS2Challenge).props.challengeWindowSize).toEqual( + '02' + ); }); test('new challenge action gets challengeWindowSize from handleAction config', async () => { @@ -124,14 +131,19 @@ describe('Dropin', () => { challengeWindowSize: '03' }); expect(dropin.componentFromAction instanceof ThreeDS2Challenge).toEqual(true); - expect((dropin.componentFromAction as ThreeDS2Challenge).props.challengeWindowSize).toEqual('03'); + expect((dropin.componentFromAction as unknown as ThreeDS2Challenge).props.challengeWindowSize).toEqual( + '03' + ); }); }); describe('Instant Payments feature', () => { test('formatProps formats instantPaymentTypes removing duplicates and invalid values', async () => { - // @ts-ignore Testing invalid interface - const dropin = new Dropin({ core: checkout, instantPaymentTypes: ['paywithgoogle', 'paywithgoogle', 'paypal', 'alipay'] }); + const dropin = new Dropin({ + core: checkout, + // @ts-ignore Valid test case + instantPaymentTypes: ['paywithgoogle', 'paywithgoogle', 'paypal', 'alipay'] + }); expect(dropin.props.instantPaymentTypes).toStrictEqual(['paywithgoogle']); }); }); diff --git a/packages/lib/src/components/internal/UIElement/UIElement.test.ts b/packages/lib/src/components/internal/UIElement/UIElement.test.ts index 5ec1c42f7d..8218d3b110 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.test.ts +++ b/packages/lib/src/components/internal/UIElement/UIElement.test.ts @@ -164,7 +164,7 @@ describe('UIElement', () => { expect(actionComponent instanceof ThreeDS2DeviceFingerprint).toEqual(true); expect(actionComponent.props.elementRef).not.toBeDefined(); - expect((actionComponent as ThreeDS2DeviceFingerprint).props.showSpinner).toEqual(true); + expect((actionComponent as unknown as ThreeDS2DeviceFingerprint).props.showSpinner).toEqual(true); expect(actionComponent.props.statusType).toEqual('loading'); expect(actionComponent.props.isDropin).toBe(false); }); @@ -192,7 +192,7 @@ describe('UIElement', () => { expect(actionComponent.props.elementRef).not.toBeDefined(); expect(actionComponent.props.statusType).toEqual('custom'); expect(actionComponent.props.isDropin).toBe(false); - expect((actionComponent as ThreeDS2Challenge).props.challengeWindowSize).toEqual('02'); + expect((actionComponent as unknown as ThreeDS2Challenge).props.challengeWindowSize).toEqual('02'); }); test('should throw Error if merchant passes the whole response object', async () => { @@ -231,25 +231,13 @@ describe('UIElement', () => { }); describe('submit()', () => { - test('should close active payment method if submit is called by instant payment method', () => { - const onSubmit = jest.fn(); - const elementRef = { closeActivePaymentMethod: jest.fn(), setStatus: jest.fn() }; - const element = new MyElement({ core, isInstantPayment: true, onSubmit, elementRef }); - - jest.spyOn(element, 'isValid', 'get').mockReturnValue(true); - - element.submit(); - - expect(elementRef.closeActivePaymentMethod).toHaveBeenCalledTimes(1); - }); - - test('should trigger showValidation() and not call onSubmit() if component is not valid', () => { + test('should trigger showValidation() and not call makePaymentsCall() if component is not valid', () => { const showValidation = jest.fn(); const element = new MyElement({ core: core }); // @ts-ignore Checking that internal method is not reached - const onSubmitSpy = jest.spyOn(element, 'onSubmit'); + const makePaymentsCallSpy = jest.spyOn(element, 'makePaymentsCall'); const componentRef = { showValidation @@ -259,7 +247,7 @@ describe('UIElement', () => { element.submit(); expect(showValidation).toBeCalledTimes(1); - expect(onSubmitSpy).not.toHaveBeenCalled(); + expect(makePaymentsCallSpy).not.toHaveBeenCalled(); }); }); }); From 02756efb717b9476e69e058d2b22ccf90dd58aa5 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Wed, 17 Jan 2024 07:40:52 -0300 Subject: [PATCH 30/55] tests: applepay --- .../src/components/ApplePay/ApplePay.test.ts | 350 +++++++++++++++++- packages/lib/src/components/ApplePay/utils.ts | 2 +- 2 files changed, 350 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.test.ts b/packages/lib/src/components/ApplePay/ApplePay.test.ts index 69a5605e0c..93815334ec 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.test.ts +++ b/packages/lib/src/components/ApplePay/ApplePay.test.ts @@ -1,6 +1,350 @@ import ApplePay from './ApplePay'; +import ApplePayService from './ApplePayService'; +import { mock } from 'jest-mock-extended'; + +jest.mock('./ApplePayService'); + +beforeEach(() => { + // @ts-ignore 'mockClear' is provided by jest.mock + ApplePayService.mockClear(); + jest.resetModules(); + jest.resetAllMocks(); + + window.ApplePaySession = { + // @ts-ignore Mock ApplePaySession.STATUS_SUCCESS STATUS_FAILURE + STATUS_SUCCESS: 1, + STATUS_FAILURE: 0, + supportsVersion: () => true + }; +}); describe('ApplePay', () => { + describe('submit()', () => { + test('should forward apple pay error (if available) to ApplePay if payment fails', async () => { + const onPaymentFailedMock = jest.fn(); + const error = mock<ApplePayJS.ApplePayError>(); + const event = mock<ApplePayJS.ApplePayPaymentAuthorizedEvent>({ + payment: { + token: { + paymentData: 'payment-data' + } + } + }); + + const applepay = new ApplePay({ + core: global.core, + countryCode: 'US', + amount: { currency: 'EUR', value: 2000 }, + onPaymentFailed: onPaymentFailedMock, + onSubmit(state, component, actions) { + actions.resolve({ + resultCode: 'Refused', + error: { + applePayError: error + } + }); + } + }); + + applepay.submit(); + + // Session initialized + await new Promise(process.nextTick); + expect(jest.spyOn(ApplePayService.prototype, 'begin')).toHaveBeenCalledTimes(1); + + // Trigger ApplePayService onPaymentAuthorized property + // @ts-ignore ApplePayService is mocked + const onPaymentAuthorized = ApplePayService.mock.calls[0][1].onPaymentAuthorized; + const resolveMock = jest.fn(); + const rejectMock = jest.fn(); + onPaymentAuthorized(resolveMock, rejectMock, event); + + await new Promise(process.nextTick); + expect(rejectMock).toHaveBeenCalledWith({ + errors: [error], + status: 0 + }); + + expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); + }); + }); + describe('onOrderTrackingRequest()', () => { + test('should collect order details and pass it to Apple', async () => { + const event = mock<ApplePayJS.ApplePayPaymentAuthorizedEvent>({ + payment: { + token: { + paymentData: 'payment-data' + } + } + }); + const onPaymentCompletedMock = jest.fn(); + const onOrderTrackingRequestMock = jest.fn().mockImplementation(resolve => { + const orderDetails = { + orderTypeIdentifier: 'orderTypeIdentifier', + orderIdentifier: 'orderIdentifier', + webServiceURL: 'webServiceURL', + authenticationToken: 'authenticationToken' + }; + resolve(orderDetails); + }); + + const applepay = new ApplePay({ + core: global.core, + countryCode: 'US', + amount: { currency: 'EUR', value: 2000 }, + onOrderTrackingRequest: onOrderTrackingRequestMock, + onPaymentCompleted: onPaymentCompletedMock + }); + + jest.spyOn(applepay as any, 'makePaymentsCall').mockResolvedValue({ + resultCode: 'Authorized' + }); + + applepay.submit(); + + await new Promise(process.nextTick); + + // Trigger onPaymentAuthorized callback + // @ts-ignore ApplePayService IS mocked + const onPaymentAuthorized = ApplePayService.mock.calls[0][1].onPaymentAuthorized; + const resolveMock = jest.fn(); + const rejectMock = jest.fn(); + onPaymentAuthorized(resolveMock, rejectMock, event); + + await new Promise(process.nextTick); + expect(onOrderTrackingRequestMock).toHaveBeenCalledTimes(1); + + expect(resolveMock).toHaveBeenCalledWith({ + status: 1, + orderDetails: { + orderTypeIdentifier: 'orderTypeIdentifier', + orderIdentifier: 'orderIdentifier', + webServiceURL: 'webServiceURL', + authenticationToken: 'authenticationToken' + } + }); + + expect(onPaymentCompletedMock).toHaveBeenCalledTimes(1); + }); + + test('should continue the payment if order details is omitted when resolving', async () => { + const event = mock<ApplePayJS.ApplePayPaymentAuthorizedEvent>({ + payment: { + token: { + paymentData: 'payment-data' + } + } + }); + const onPaymentCompletedMock = jest.fn(); + const onOrderTrackingRequestMock = jest.fn().mockImplementation(resolve => { + resolve(); + }); + + const applepay = new ApplePay({ + core: global.core, + countryCode: 'US', + amount: { currency: 'EUR', value: 2000 }, + onOrderTrackingRequest: onOrderTrackingRequestMock, + onPaymentCompleted: onPaymentCompletedMock + }); + + jest.spyOn(applepay as any, 'makePaymentsCall').mockResolvedValue({ + resultCode: 'Authorized' + }); + + applepay.submit(); + + await new Promise(process.nextTick); + + // Trigger onPaymentAuthorized callback + // @ts-ignore ApplePayService IS mocked + const onPaymentAuthorized = ApplePayService.mock.calls[0][1].onPaymentAuthorized; + const resolveMock = jest.fn(); + const rejectMock = jest.fn(); + onPaymentAuthorized(resolveMock, rejectMock, event); + + await new Promise(process.nextTick); + expect(onOrderTrackingRequestMock).toHaveBeenCalledTimes(1); + + expect(resolveMock).toHaveBeenCalledWith({ + status: 1 + }); + + expect(onPaymentCompletedMock).toHaveBeenCalledTimes(1); + }); + + test('should continue the payment if something goes wrong and order details callback is rejected', async () => { + const event = mock<ApplePayJS.ApplePayPaymentAuthorizedEvent>({ + payment: { + token: { + paymentData: 'payment-data' + } + } + }); + const onPaymentCompletedMock = jest.fn(); + const onOrderTrackingRequestMock = jest.fn().mockImplementation((resolve, reject) => { + reject(); + }); + + const applepay = new ApplePay({ + core: global.core, + countryCode: 'US', + amount: { currency: 'EUR', value: 2000 }, + onOrderTrackingRequest: onOrderTrackingRequestMock, + onPaymentCompleted: onPaymentCompletedMock + }); + + jest.spyOn(applepay as any, 'makePaymentsCall').mockResolvedValue({ + resultCode: 'Authorized' + }); + + applepay.submit(); + + await new Promise(process.nextTick); + + // Trigger onPaymentAuthorized callback + // @ts-ignore ApplePayService IS mocked + const onPaymentAuthorized = ApplePayService.mock.calls[0][1].onPaymentAuthorized; + const resolveMock = jest.fn(); + const rejectMock = jest.fn(); + onPaymentAuthorized(resolveMock, rejectMock, event); + + await new Promise(process.nextTick); + expect(onOrderTrackingRequestMock).toHaveBeenCalledTimes(1); + + expect(resolveMock).toHaveBeenCalledWith({ + status: 1 + }); + + expect(onPaymentCompletedMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('onAuthorized()', () => { + test('should provide event and formatted data, then reject payment', async () => { + const event = mock<ApplePayJS.ApplePayPaymentAuthorizedEvent>({ + payment: { + billingContact: { + addressLines: ['802 Richardon Drive', 'Brooklyn'], + locality: 'New York', + administrativeArea: 'NY', + postalCode: '11213', + countryCode: 'US', + country: 'United States', + givenName: 'Jonny', + familyName: 'Smithson', + phoneticFamilyName: '', + phoneticGivenName: '', + subAdministrativeArea: '', + subLocality: '' + }, + shippingContact: { + addressLines: ['1 Infinite Loop', 'Unit 100'], + locality: 'Cupertino', + administrativeArea: 'CA', + postalCode: '95014', + countryCode: 'US', + country: 'United States', + givenName: 'John', + familyName: 'Appleseed', + phoneticFamilyName: '', + phoneticGivenName: '', + subAdministrativeArea: '', + subLocality: '' + }, + token: { + paymentData: 'payment-data' + } + } + }); + + const onPaymentFailedMock = jest.fn(); + const onChangeMock = jest.fn(); + const onAuthorizedMock = jest.fn().mockImplementation((_data, actions) => { + actions.reject(); + }); + + const applepay = new ApplePay({ + core: global.core, + countryCode: 'US', + amount: { currency: 'EUR', value: 2000 }, + onAuthorized: onAuthorizedMock, + onChange: onChangeMock, + onPaymentFailed: onPaymentFailedMock + }); + + applepay.submit(); + + // Session initialized + await new Promise(process.nextTick); + expect(jest.spyOn(ApplePayService.prototype, 'begin')).toHaveBeenCalledTimes(1); + + // Trigger onPaymentAuthorized callback + // @ts-ignore ApplePayService IS mocked + const onPaymentAuthorized = ApplePayService.mock.calls[0][1].onPaymentAuthorized; + const resolveMock = jest.fn(); + const rejectMock = jest.fn(); + onPaymentAuthorized(resolveMock, rejectMock, event); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + expect(onChangeMock.mock.calls[0][0].data).toStrictEqual({ + billingAddress: { + city: 'New York', + country: 'US', + houseNumberOrName: 'ZZ', + postalCode: '11213', + stateOrProvince: 'NY', + street: '802 Richardon Drive Brooklyn' + }, + clientStateDataIndicator: true, + deliveryAddress: { + city: 'Cupertino', + country: 'US', + firstName: 'John', + houseNumberOrName: 'ZZ', + lastName: 'Appleseed', + postalCode: '95014', + stateOrProvince: 'CA', + street: '1 Infinite Loop Unit 100' + }, + paymentMethod: { + applePayToken: 'InBheW1lbnQtZGF0YSI=', + checkoutAttemptId: 'do-not-track', + type: 'applepay' + } + }); + + const data = onAuthorizedMock.mock.calls[0][0]; + expect(data.authorizedEvent).toBe(event); + expect(data.billingAddress).toStrictEqual({ + city: 'New York', + country: 'US', + houseNumberOrName: 'ZZ', + postalCode: '11213', + stateOrProvince: 'NY', + street: '802 Richardon Drive Brooklyn' + }); + expect(data.deliveryAddress).toStrictEqual({ + city: 'Cupertino', + country: 'US', + firstName: 'John', + houseNumberOrName: 'ZZ', + lastName: 'Appleseed', + postalCode: '95014', + stateOrProvince: 'CA', + street: '1 Infinite Loop Unit 100' + }); + + await new Promise(process.nextTick); + expect(rejectMock).toHaveBeenCalledWith({ + errors: undefined, + status: 0 + }); + + expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); + }); + }); + describe('formatProps', () => { test('accepts an amount in a regular format', () => { const applepay = new ApplePay({ @@ -19,7 +363,11 @@ describe('ApplePay', () => { }); test('uses merchantName if no totalPriceLabel was defined', () => { - const applepay = new ApplePay({ core: global.core, countryCode: 'US', configuration: { merchantName: 'Test' } }); + const applepay = new ApplePay({ + core: global.core, + countryCode: 'US', + configuration: { merchantName: 'Test' } + }); expect(applepay.props.totalPriceLabel).toEqual('Test'); }); diff --git a/packages/lib/src/components/ApplePay/utils.ts b/packages/lib/src/components/ApplePay/utils.ts index 71208aa9a0..85532d8dec 100644 --- a/packages/lib/src/components/ApplePay/utils.ts +++ b/packages/lib/src/components/ApplePay/utils.ts @@ -58,7 +58,7 @@ export function formatApplePayContactToAdyenAddressFormat( country: paymentContact.countryCode, houseNumberOrName: 'ZZ', postalCode: paymentContact.postalCode, - street: paymentContact.addressLines.join(' ').trim(), + street: paymentContact.addressLines?.join(' ').trim(), ...(paymentContact.administrativeArea && { stateOrProvince: paymentContact.administrativeArea }), ...(isDeliveryAddress && { firstName: paymentContact.givenName, From 163dbcdabc00ac1cbde07fe8a451ad5167969bac Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Wed, 17 Jan 2024 12:03:04 -0300 Subject: [PATCH 31/55] tests: googlepay --- .../components/GooglePay/GooglePay.test.ts | 328 +++++++++++++++++- .../playground/src/pages/Wallets/Wallets.js | 4 +- 2 files changed, 325 insertions(+), 7 deletions(-) diff --git a/packages/lib/src/components/GooglePay/GooglePay.test.ts b/packages/lib/src/components/GooglePay/GooglePay.test.ts index 74032bb339..a5e3fd83d5 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.test.ts +++ b/packages/lib/src/components/GooglePay/GooglePay.test.ts @@ -1,10 +1,325 @@ import GooglePay from './GooglePay'; +import GooglePayService from './GooglePayService'; + +jest.mock('./GooglePayService'); + +beforeEach(() => { + // @ts-ignore 'mockClear' is provided by jest.mock + GooglePayService.mockClear(); + jest.resetModules(); + jest.resetAllMocks(); +}); + +let googlePaymentData: Partial<google.payments.api.PaymentData> = {}; + +beforeEach(() => { + googlePaymentData = { + apiVersionMinor: 0, + apiVersion: 2, + paymentMethodData: { + description: 'Visa •••• 1111', + tokenizationData: { + type: 'PAYMENT_GATEWAY', + token: 'google-pay-token' + }, + type: 'CARD', + info: { + cardNetwork: 'VISA', + cardDetails: '1111', + // @ts-ignore Complaining about missing fields, although Google returned only these + billingAddress: { + countryCode: 'US', + postalCode: '94043', + name: 'Card Holder Name' + } + } + }, + shippingAddress: { + address3: '', + sortingCode: '', + address2: '', + countryCode: 'US', + address1: '1600 Amphitheatre Parkway1', + postalCode: '94043', + name: 'US User', + locality: 'Mountain View', + administrativeArea: 'CA' + }, + email: 'shopper@gmail.com' + }; +}); describe('GooglePay', () => { - describe('get data', () => { - test('always returns a type', () => { - const gpay = new GooglePay({ core: global.core }); - expect(gpay.data.paymentMethod.type).toBe('googlepay'); + describe('submit()', () => { + test('should make the payments call passing deliveryAddress and billingAddress', async () => { + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Authorized' + }); + }); + const onPaymentCompletedMock = jest.fn(); + + const gpay = new GooglePay({ + core: global.core, + onSubmit: onSubmitMock, + onPaymentCompleted: onPaymentCompletedMock + }); + + // @ts-ignore GooglePayService is mocked + const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized; + const promise = onPaymentAuthorized(googlePaymentData); + + await new Promise(process.nextTick); + expect(onSubmitMock).toHaveBeenCalledTimes(1); + + const state = onSubmitMock.mock.calls[0][0]; + + expect(state.data.origin).toBe('http://localhost'); + expect(state.data.paymentMethod).toStrictEqual({ + checkoutAttemptId: 'do-not-track', + googlePayCardNetwork: 'VISA', + googlePayToken: 'google-pay-token', + type: 'googlepay' + }); + expect(state.data.deliveryAddress).toStrictEqual({ + city: 'Mountain View', + country: 'US', + firstName: 'US User', + houseNumberOrName: 'ZZ', + postalCode: '94043', + stateOrProvince: 'CA', + street: '1600 Amphitheatre Parkway1' + }); + expect(state.data.billingAddress).toStrictEqual({ + city: '', + country: 'US', + houseNumberOrName: 'ZZ', + postalCode: '94043', + street: '' + }); + expect(state.data.browserInfo).toStrictEqual({ + acceptHeader: '*/*', + colorDepth: 24, + javaEnabled: false, + language: 'en-US', + screenHeight: '', + screenWidth: '', + timeZoneOffset: 360, + userAgent: 'Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/20.0.3' + }); + + await new Promise(process.nextTick); + + expect(promise).resolves.toEqual({ + transactionState: 'SUCCESS' + }); + + expect(onPaymentCompletedMock).toHaveBeenCalledWith({ resultCode: 'Authorized' }, gpay); + }); + + test('should not add deliveryAddress and billingAddress if they are not available', async () => { + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Authorized' + }); + }); + + new GooglePay({ + core: global.core, + onSubmit: onSubmitMock + }); + + // @ts-ignore GooglePayService is mocked + const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized; + const googlePaymentDataWithoutAddresses = { ...googlePaymentData }; + delete googlePaymentDataWithoutAddresses.shippingAddress; + delete googlePaymentDataWithoutAddresses.paymentMethodData.info.billingAddress; + onPaymentAuthorized(googlePaymentDataWithoutAddresses); + + await new Promise(process.nextTick); + expect(onSubmitMock).toHaveBeenCalledTimes(1); + + const state = onSubmitMock.mock.calls[0][0]; + + expect(state.data.origin).toBe('http://localhost'); + expect(state.data.paymentMethod).toStrictEqual({ + checkoutAttemptId: 'do-not-track', + googlePayCardNetwork: 'VISA', + googlePayToken: 'google-pay-token', + type: 'googlepay' + }); + expect(state.data.deliveryAddress).toBeUndefined(); + expect(state.data.billingAddress).toBeUndefined(); + }); + + test('should pass error to GooglePay if payment failed', async () => { + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Refused', + error: { + googlePayError: 'Insufficient funds' + } + }); + }); + const onPaymentFailedMock = jest.fn(); + + const gpay = new GooglePay({ + core: global.core, + onSubmit: onSubmitMock, + onPaymentFailed: onPaymentFailedMock + }); + + // @ts-ignore GooglePayService is mocked + const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized; + const promise = onPaymentAuthorized(googlePaymentData); + + await new Promise(process.nextTick); + + expect(promise).resolves.toEqual({ + error: { + intent: 'PAYMENT_AUTHORIZATION', + message: 'Insufficient funds', + reason: 'OTHER_ERROR' + }, + transactionState: 'ERROR' + }); + + expect(onPaymentFailedMock).toHaveBeenCalledWith( + { resultCode: 'Refused', error: { googlePayError: 'Insufficient funds' } }, + gpay + ); + }); + }); + + describe('onAuthorized()', () => { + const event = { + authorizedEvent: { + apiVersionMinor: 0, + apiVersion: 2, + paymentMethodData: { + description: 'Visa •••• 1111', + tokenizationData: { + type: 'PAYMENT_GATEWAY', + token: 'google-pay-token' + }, + type: 'CARD', + info: { + cardNetwork: 'VISA', + cardDetails: '1111', + billingAddress: { + countryCode: 'US', + postalCode: '94043', + name: 'Card Holder Name' + } + } + }, + shippingAddress: { + address3: '', + sortingCode: '', + address2: '', + countryCode: 'US', + address1: '1600 Amphitheatre Parkway1', + postalCode: '94043', + name: 'US User', + locality: 'Mountain View', + administrativeArea: 'CA' + }, + email: 'shopper@gmail.com' + }, + billingAddress: { + postalCode: '94043', + country: 'US', + street: '', + houseNumberOrName: 'ZZ', + city: '' + }, + deliveryAddress: { + postalCode: '94043', + country: 'US', + street: '1600 Amphitheatre Parkway1', + houseNumberOrName: 'ZZ', + city: 'Mountain View', + stateOrProvince: 'CA', + firstName: 'US User' + } + }; + + test('should provide GooglePay auth event and formatted data', () => { + const onAuthorizedMock = jest.fn(); + new GooglePay({ core: global.core, onAuthorized: onAuthorizedMock }); + + // @ts-ignore GooglePayService is mocked + const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized; + onPaymentAuthorized(googlePaymentData); + + expect(onAuthorizedMock.mock.calls[0][0]).toStrictEqual(event); + }); + + test('should pass error to GooglePay if the action.reject happens on onAuthorized', async () => { + const onAuthorizedMock = jest.fn().mockImplementation((_data, actions) => { + console.log('reject'); + actions.reject('Not supported network scheme'); + }); + const onPaymentFailedMock = jest.fn(); + + new GooglePay({ + core: global.core, + onAuthorized: onAuthorizedMock, + onPaymentFailed: onPaymentFailedMock + }); + + // @ts-ignore GooglePayService is mocked + const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized; + const promise = onPaymentAuthorized(googlePaymentData); + + expect(promise).resolves.toEqual({ + error: { + intent: 'PAYMENT_AUTHORIZATION', + message: 'Not supported network scheme', + reason: 'OTHER_ERROR' + }, + transactionState: 'ERROR' + }); + + await new Promise(process.nextTick); + expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); + }); + + test('should continue the payment flow if action.resolve happens on onAuthorized', async () => { + const onAuthorizedMock = jest.fn().mockImplementation((_data, actions) => { + actions.resolve(); + }); + const onPaymentCompletedMock = jest.fn(); + + const gpay = new GooglePay({ + core: global.core, + onAuthorized: onAuthorizedMock, + onPaymentCompleted: onPaymentCompletedMock + }); + + const paymentCall = jest.spyOn(gpay as any, 'makePaymentsCall'); + + // @ts-ignore GooglePayService is mocked + const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized; + onPaymentAuthorized(googlePaymentData); + + await new Promise(process.nextTick); + expect(paymentCall).toHaveBeenCalledTimes(1); + }); + + test('should make the payments call if onAuthorized is not provided', async () => { + const gpay = new GooglePay({ + core: global.core + }); + + const paymentCall = jest.spyOn(gpay as any, 'makePaymentsCall'); + + // @ts-ignore GooglePayService is mocked + const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized; + onPaymentAuthorized(googlePaymentData); + + await new Promise(process.nextTick); + expect(paymentCall).toHaveBeenCalledTimes(1); }); }); @@ -44,7 +359,10 @@ describe('GooglePay', () => { }); test('Retrieves merchantId from configuration', () => { - const gpay = new GooglePay({ core: global.core, configuration: { merchantId: 'abcdef', gatewayMerchantId: 'TestMerchant' } }); + const gpay = new GooglePay({ + core: global.core, + configuration: { merchantId: 'abcdef', gatewayMerchantId: 'TestMerchant' } + }); expect(gpay.props.configuration.merchantId).toEqual('abcdef'); }); diff --git a/packages/playground/src/pages/Wallets/Wallets.js b/packages/playground/src/pages/Wallets/Wallets.js index a227c7fa94..fb9c3a31d5 100644 --- a/packages/playground/src/pages/Wallets/Wallets.js +++ b/packages/playground/src/pages/Wallets/Wallets.js @@ -195,9 +195,9 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = // Shopper info (optional) emailRequired: true, - // billingAddressRequired: true, + billingAddressRequired: true, - // shippingAddressRequired: true, + shippingAddressRequired: true, // shippingAddressParameters: {}, // https://developers.google.com/pay/api/web/reference/object#ShippingAddressParameters // Button config (optional) From 7e863262c64fd10a811abe2ae5b055c2ba7ca5d8 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Wed, 17 Jan 2024 12:20:46 -0300 Subject: [PATCH 32/55] fix: temp stuff --- .../components/internal/UIElement/UIElement.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/lib/src/components/internal/UIElement/UIElement.test.ts b/packages/lib/src/components/internal/UIElement/UIElement.test.ts index 8218d3b110..05e43229f9 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.test.ts +++ b/packages/lib/src/components/internal/UIElement/UIElement.test.ts @@ -249,5 +249,18 @@ describe('UIElement', () => { expect(showValidation).toBeCalledTimes(1); expect(makePaymentsCallSpy).not.toHaveBeenCalled(); }); + + // test.todo('should make successfull payment using advanced flow'); + // test.todo('should make successfull payment using sessions flow'); + // test.todo('should call onPaymentFailed if payment contains resultCode non-sucessfull result code'); + // test.todo('should call onPaymentFailed if payment is rejected'); + // test.todo('should call component.handleAction if payment is resolved with action'); }); + + // describe('Handling additional details', () => { + // test.todo('should make successfull payment using advanced flow'); + // test.todo('should make successfull payment using sessions flow'); + // test.todo('should call onPaymentFailed if payment contains resultCode non-sucessfull result code'); + // test.todo('should call onPaymentFailed if payment is rejected'); + // }); }); From cc3327fa2832f2f05f7e610f7d8870614baa413e Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Fri, 19 Jan 2024 12:16:52 -0300 Subject: [PATCH 33/55] test: uielement, handling order, actions, additional details --- .../internal/UIElement/UIElement.test.ts | 469 +++++++++++++++++- .../internal/UIElement/UIElement.tsx | 17 +- .../components/internal/UIElement/utils.ts | 4 +- .../core/CheckoutSession/CheckoutSession.ts | 3 +- packages/lib/src/core/core.ts | 4 +- packages/lib/src/core/types.ts | 9 +- packages/lib/src/types/global-types.ts | 12 + 7 files changed, 487 insertions(+), 31 deletions(-) diff --git a/packages/lib/src/components/internal/UIElement/UIElement.test.ts b/packages/lib/src/components/internal/UIElement/UIElement.test.ts index 05e43229f9..f887d77622 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.test.ts +++ b/packages/lib/src/components/internal/UIElement/UIElement.test.ts @@ -1,8 +1,9 @@ -import UIElement from './UIElement'; +import { UIElement } from './UIElement'; import { ICore } from '../../../core/types'; -import { mockDeep, mockReset } from 'jest-mock-extended'; +import { any, mockDeep } from 'jest-mock-extended'; import { AdyenCheckout, ThreeDS2Challenge, ThreeDS2DeviceFingerprint } from '../../../index'; import { UIElementProps } from './types'; +import AdyenCheckoutError from '../../../core/Errors/AdyenCheckoutError'; interface MyElementProps extends UIElementProps { challengeWindowSize?: string; @@ -27,9 +28,9 @@ class MyElement extends UIElement<MyElementProps> { const submitMock = jest.fn(); (global as any).HTMLFormElement.prototype.submit = () => submitMock; -const core = mockDeep<ICore>(); +let core; beforeEach(() => { - mockReset(core); + core = mockDeep<ICore>(); }); describe('UIElement', () => { @@ -250,17 +251,455 @@ describe('UIElement', () => { expect(makePaymentsCallSpy).not.toHaveBeenCalled(); }); - // test.todo('should make successfull payment using advanced flow'); - // test.todo('should make successfull payment using sessions flow'); - // test.todo('should call onPaymentFailed if payment contains resultCode non-sucessfull result code'); - // test.todo('should call onPaymentFailed if payment is rejected'); - // test.todo('should call component.handleAction if payment is resolved with action'); + test('should make successfully payment using advanced flow', async () => { + const onPaymentCompletedMock = jest.fn(); + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Authorized' + }); + }); + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + jest.spyOn(MyElement.prototype, 'data', 'get').mockReturnValue({ + clientStateDataIndicator: true, + paymentMethod: { + type: 'payment-type' + } + }); + + const element = new MyElement({ + core: core, + onSubmit: onSubmitMock, + onPaymentCompleted: onPaymentCompletedMock + }); + + element.submit(); + + await new Promise(process.nextTick); + + expect(onPaymentCompletedMock).toHaveBeenCalledTimes(1); + expect(onPaymentCompletedMock).toHaveBeenCalledWith({ resultCode: 'Authorized' }, element); + }); + + test('should make successfull payment using sessions flow', async () => { + const onPaymentCompletedMock = jest.fn(); + + core.session.submitPayment.calledWith(any()).mockResolvedValue({ + resultCode: 'Authorised', + sessionData: 'session-data', + sessionResult: 'session-result' + }); + + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + jest.spyOn(MyElement.prototype, 'data', 'get').mockReturnValue({ + clientStateDataIndicator: true, + paymentMethod: { + type: 'payment-type' + } + }); + + const element = new MyElement({ + core: core, + onPaymentCompleted: onPaymentCompletedMock + }); + + element.submit(); + + await new Promise(process.nextTick); + + expect(onPaymentCompletedMock).toHaveBeenCalledTimes(1); + expect(onPaymentCompletedMock).toHaveBeenCalledWith( + { + resultCode: 'Authorised', + sessionData: 'session-data', + sessionResult: 'session-result' + }, + element + ); + }); + + test('should call onPaymentFailed if payment contains non-successful result code', async () => { + const onPaymentFailedMock = jest.fn(); + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Refused' + }); + }); + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + jest.spyOn(MyElement.prototype, 'data', 'get').mockReturnValue({ + clientStateDataIndicator: true, + paymentMethod: { + type: 'payment-type' + } + }); + + const element = new MyElement({ + core: core, + onSubmit: onSubmitMock, + onPaymentFailed: onPaymentFailedMock + }); + + element.submit(); + + await new Promise(process.nextTick); + + expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); + expect(onPaymentFailedMock).toHaveBeenCalledWith({ resultCode: 'Refused' }, element); + }); + + test('should call onPaymentFailed if payment is rejected', async () => { + const onPaymentFailedMock = jest.fn(); + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.reject(); + }); + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + + const element = new MyElement({ + core: core, + onSubmit: onSubmitMock, + onPaymentFailed: onPaymentFailedMock + }); + + element.submit(); + + await new Promise(process.nextTick); + + expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); + expect(onPaymentFailedMock).toHaveBeenCalledWith(undefined, element); + }); + + test('should call component.handleAction if payment is resolved with action', async () => { + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Pending', + action: { + type: 'sdk', + paymentMethodType: 'payment-type', + paymentData: 'payment-data' + } + }); + }); + + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + + const element = new MyElement({ + core: core, + onSubmit: onSubmitMock + }); + + const handleActionSpy = jest.spyOn(element, 'handleAction'); + + element.submit(); + + await new Promise(process.nextTick); + + expect(handleActionSpy).toHaveBeenCalledTimes(1); + expect(handleActionSpy).toHaveBeenCalledWith({ + type: 'sdk', + paymentMethodType: 'payment-type', + paymentData: 'payment-data' + }); + }); + + test('should trigger core.update if there is pending order when using sessions', async () => { + const order = { + amount: { + currency: 'EUR', + value: 2001 + }, + expiresAt: '2023-10-10T13:12:59.00Z', + orderData: 'order-mock', + pspReference: 'MHCDBZCH4NF96292', + reference: 'ABC123', + remainingAmount: { + currency: 'EUR', + value: 100 + } + }; + + const onOrderUpdatedMock = jest.fn(); + + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + core.update.calledWith(any()).mockResolvedValue(core); + core.session.submitPayment.calledWith(any()).mockResolvedValue({ + resultCode: 'Pending', + // @ts-ignore ADD ORDER TO SESSION CHECKOUT RESPONSE + order, + sessionData: 'session-data', + sessionResult: 'session-result' + }); + + const element = new MyElement({ + core: core, + onOrderUpdated: onOrderUpdatedMock + }); + + element.submit(); + + await new Promise(process.nextTick); + + expect(core.update).toHaveBeenCalledTimes(1); + expect(core.update).toHaveBeenCalledWith({ order }); + + expect(onOrderUpdatedMock).toHaveBeenCalledTimes(1); + expect(onOrderUpdatedMock).toHaveBeenCalledWith({ order }); + }); + + test('should trigger onPaymentMethodsRequest if there is a pending order when using advanced flow', async () => { + const order = { + amount: { + currency: 'EUR', + value: 2001 + }, + expiresAt: '2023-10-10T13:12:59.00Z', + orderData: 'order-mock', + pspReference: 'MHCDBZCH4NF96292', + reference: 'ABC123', + remainingAmount: { + currency: 'EUR', + value: 100 + } + }; + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Pending', + order + }); + }); + const onPaymentMethodsRequestMock = jest.fn().mockImplementation((data, actions) => { + actions.resolve({ + paymentMethods: [], + storedPaymentMethods: [] + }); + }); + const onOrderUpdatedMock = jest.fn(); + + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + core.update.calledWith(any()).mockResolvedValue(core); + core.options.locale = 'en-US'; + core.session = null; + + const element = new MyElement({ + core: core, + onSubmit: onSubmitMock, + onPaymentMethodsRequest: onPaymentMethodsRequestMock, + onOrderUpdated: onOrderUpdatedMock + }); + + element.submit(); + + await new Promise(process.nextTick); + + expect(onPaymentMethodsRequestMock).toHaveBeenCalledTimes(1); + expect(onPaymentMethodsRequestMock.mock.calls[0][0]).toStrictEqual({ + order: { + orderData: 'order-mock', + pspReference: 'MHCDBZCH4NF96292' + }, + locale: 'en-US' + }); + + expect(core.update).toHaveBeenCalledTimes(1); + expect(core.update).toHaveBeenCalledWith({ + paymentMethodsResponse: { + paymentMethods: [], + storedPaymentMethods: [] + }, + order, + amount: order.remainingAmount + }); + + expect(onOrderUpdatedMock).toHaveBeenCalledTimes(1); + expect(onOrderUpdatedMock).toHaveBeenCalledWith({ order }); + }); + + test('should throw an error if onPaymentMethodsRequest is not implemented, although the flow will continue', async () => { + const order = { + amount: { + currency: 'EUR', + value: 2001 + }, + expiresAt: '2023-10-10T13:12:59.00Z', + orderData: 'order-mock', + pspReference: 'MHCDBZCH4NF96292', + reference: 'ABC123', + remainingAmount: { + currency: 'EUR', + value: 100 + } + }; + const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Pending', + order + }); + }); + const onOrderUpdatedMock = jest.fn(); + const onErrorMock = jest.fn(); + + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + core.update.calledWith(any()).mockResolvedValue(core); + core.options.locale = 'en-US'; + core.session = null; + + const element = new MyElement({ + core: core, + onSubmit: onSubmitMock, + onOrderUpdated: onOrderUpdatedMock, + onError: onErrorMock + }); + + element.submit(); + + await new Promise(process.nextTick); + + expect(onErrorMock).toHaveBeenCalledTimes(1); + expect(onErrorMock.mock.calls[0][0]).toBeInstanceOf(AdyenCheckoutError); + + expect(core.update).toHaveBeenCalledTimes(1); + expect(core.update).toHaveBeenCalledWith({ + order, + amount: order.remainingAmount + }); + + expect(onOrderUpdatedMock).toHaveBeenCalledTimes(1); + expect(onOrderUpdatedMock).toHaveBeenCalledWith({ order }); + }); }); - // describe('Handling additional details', () => { - // test.todo('should make successfull payment using advanced flow'); - // test.todo('should make successfull payment using sessions flow'); - // test.todo('should call onPaymentFailed if payment contains resultCode non-sucessfull result code'); - // test.todo('should call onPaymentFailed if payment is rejected'); - // }); + describe('[Internal] handleAdditionalDetails()', () => { + test('should make successfully payment/details using advanced flow', async () => { + const onPaymentCompletedMock = jest.fn(); + const onAdditionalDetailsMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Authorized' + }); + }); + + const element = new MyElement({ + core: core, + onAdditionalDetails: onAdditionalDetailsMock, + onPaymentCompleted: onPaymentCompletedMock + }); + + const data = { + data: { + details: { + paymentSource: 'paypal' + }, + paymentData: 'payment-data' + } + }; + + // @ts-ignore Testing internal implementation + element.handleAdditionalDetails(data); + + await new Promise(process.nextTick); + + expect(onAdditionalDetailsMock).toHaveBeenCalledTimes(1); + expect(onAdditionalDetailsMock.mock.calls[0][0]).toEqual(data); + + expect(onPaymentCompletedMock).toHaveBeenCalledTimes(1); + expect(onPaymentCompletedMock).toHaveBeenCalledWith({ resultCode: 'Authorized' }, element); + }); + + test('should make successfully payment/details using sessions flow', async () => { + const onPaymentCompletedMock = jest.fn(); + + const element = new MyElement({ + core: core, + onPaymentCompleted: onPaymentCompletedMock + }); + + core.session.submitDetails.calledWith(any()).mockResolvedValue({ + resultCode: 'Authorised', + sessionData: 'session-data', + sessionResult: 'session-result' + }); + + const state = { + data: { + details: { + paymentSource: 'paypal' + }, + paymentData: 'payment-data' + } + }; + + // @ts-ignore Testing internal implementation + element.handleAdditionalDetails(state); + + await new Promise(process.nextTick); + + expect(core.session.submitDetails).toHaveBeenCalledTimes(1); + expect(core.session.submitDetails).toHaveBeenCalledWith(state.data); + + expect(onPaymentCompletedMock).toHaveBeenCalledTimes(1); + expect(onPaymentCompletedMock).toHaveBeenCalledWith( + { resultCode: 'Authorised', sessionData: 'session-data', sessionResult: 'session-result' }, + element + ); + }); + + test('should call onPaymentFailed if payment/details contains non-successful result code', async () => { + const onPaymentFailedMock = jest.fn(); + const onAdditionalDetailsMock = jest.fn().mockImplementation((data, component, actions) => { + actions.resolve({ + resultCode: 'Refused' + }); + }); + + const element = new MyElement({ + core: core, + onAdditionalDetails: onAdditionalDetailsMock, + onPaymentFailed: onPaymentFailedMock + }); + + const data = { + data: { + details: { + paymentSource: 'paypal' + }, + paymentData: 'payment-data' + } + }; + + // @ts-ignore Testing internal implementation + element.handleAdditionalDetails(data); + + await new Promise(process.nextTick); + + expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); + expect(onPaymentFailedMock).toHaveBeenCalledWith({ resultCode: 'Refused' }, element); + }); + + test('should call onPaymentFailed if payment is rejected', async () => { + const onPaymentFailedMock = jest.fn(); + const onAdditionalDetailsMock = jest.fn().mockImplementation((data, component, actions) => { + actions.reject(); + }); + + const element = new MyElement({ + core: core, + onAdditionalDetails: onAdditionalDetailsMock, + onPaymentFailed: onPaymentFailedMock + }); + + const data = { + data: { + details: { + paymentSource: 'paypal' + }, + paymentData: 'payment-data' + } + }; + + // @ts-ignore Testing internal implementation + element.handleAdditionalDetails(data); + + await new Promise(process.nextTick); + + expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); + expect(onPaymentFailedMock).toHaveBeenCalledWith(undefined, element); + }); + }); }); diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 5dab328a73..93f0c27c5f 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -15,7 +15,8 @@ import { PaymentMethodsResponse, CheckoutAdvancedFlowResponse, PaymentResponseData, - RawPaymentResponse + RawPaymentResponse, + AdditionalDetailsStateData } from '../../../types/global-types'; import './UIElement.scss'; import { CheckoutSessionDetailsResponse, CheckoutSessionPaymentResponse } from '../../../core/CheckoutSession/types'; @@ -222,7 +223,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> } }; - protected handleAdditionalDetails(state: any): void { + protected handleAdditionalDetails(state: AdditionalDetailsStateData): void { this.makeAdditionalDetailsCall(state) .then(sanitizeResponse) .then(verifyPaymentDidNotFail) @@ -231,7 +232,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> } private makeAdditionalDetailsCall( - state: any + state: AdditionalDetailsStateData ): Promise<CheckoutSessionDetailsResponse | CheckoutAdvancedFlowResponse> { if (this.props.setStatusAutomatically) { this.setElementStatus('loading'); @@ -243,7 +244,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> }); } - if (this.props.session) { + if (this.core.session) { return this.submitAdditionalDetailsUsingSessionsFlow(state.data); } @@ -303,14 +304,14 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> /** * Handles when the payment fails. The payment fails when: - * - adv flow: the merchant rejects the payment + * - adv flow: the merchant rejects the payment due to a critical error * - adv flow: the merchant resolves the payment with a failed resultCode - * - sessions: an error occurs during session when making the payment - * - sessions: the payment fail + * - sessions: a network error occurs when making the payment + * - sessions: the payment fails with a failed resultCode * * @param result */ - protected handleFailedResult = (result: PaymentResponseData): void => { + protected handleFailedResult = (result?: PaymentResponseData): void => { if (this.props.setStatusAutomatically) { this.setElementStatus('error'); } diff --git a/packages/lib/src/components/internal/UIElement/utils.ts b/packages/lib/src/components/internal/UIElement/utils.ts index 2466e68767..02941f4685 100644 --- a/packages/lib/src/components/internal/UIElement/utils.ts +++ b/packages/lib/src/components/internal/UIElement/utils.ts @@ -26,7 +26,9 @@ export function sanitizeResponse(response: RawPaymentResponse): PaymentResponseD * * @param paymentResponse */ -export function cleanupFinalResult(paymentResponse: PaymentResponseData): void { +export function cleanupFinalResult(paymentResponse?: PaymentResponseData): void { + if (!paymentResponse) return; + delete paymentResponse.order; delete paymentResponse.action; if (!paymentResponse.donationToken || paymentResponse.donationToken.length === 0) { diff --git a/packages/lib/src/core/CheckoutSession/CheckoutSession.ts b/packages/lib/src/core/CheckoutSession/CheckoutSession.ts index 6cc17a4b0c..647d03b03c 100644 --- a/packages/lib/src/core/CheckoutSession/CheckoutSession.ts +++ b/packages/lib/src/core/CheckoutSession/CheckoutSession.ts @@ -16,6 +16,7 @@ import { } from './types'; import cancelOrder from '../Services/sessions/cancel-order'; import { onOrderCancelData } from '../../components/Dropin/types'; +import { AdditionalDetailsStateData } from '../../types/global-types'; class Session { private readonly session: CheckoutSession; @@ -91,7 +92,7 @@ class Session { /** * Submits session payment additional details */ - submitDetails(data): Promise<CheckoutSessionDetailsResponse> { + submitDetails(data: AdditionalDetailsStateData['data']): Promise<CheckoutSessionDetailsResponse> { return submitDetails(data, this).then(response => { if (response.sessionData) { this.updateSessionData(response.sessionData); diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index ca4cbf1e6b..bb9795c9c7 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -5,7 +5,7 @@ import PaymentMethods from './ProcessResponse/PaymentMethods'; import getComponentForAction from './ProcessResponse/PaymentAction'; import { resolveEnvironment, resolveCDNEnvironment } from './Environment'; import Analytics from './Analytics'; -import { PaymentAction, PaymentResponseData } from '../types/global-types'; +import { AdditionalDetailsStateData, PaymentAction, PaymentResponseData } from '../types/global-types'; import { CoreConfiguration, ICore } from './types'; import { processGlobalOptions } from './utils'; import Session from './CheckoutSession'; @@ -114,7 +114,7 @@ class Core implements ICore { * @see {https://docs.adyen.com/online-payments/build-your-integration/?platform=Web&integration=Components&version=5.55.1#handle-the-redirect} * @param details - Details object containing the redirectResult */ - public submitDetails(details: { details: { redirectResult: string } }): void { + public submitDetails(details: AdditionalDetailsStateData['data']): void { let promise = null; if (this.options.onAdditionalDetails) { diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index ab25bba2bf..a16553e454 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -9,7 +9,8 @@ import { CheckoutAdvancedFlowResponse, PaymentMethodsRequestData, SessionsResponse, - ResultCode + ResultCode, + AdditionalDetailsStateData } from '../types/global-types'; import { AnalyticsOptions } from './Analytics/types'; import { RiskModuleOptions } from './RiskModule/RiskModule'; @@ -176,10 +177,10 @@ export interface CoreConfiguration { /** * Called when the payment fails. * - * The first parameter is poppulated when merchant is using sessions, or when the payment was rejected + * The first parameter is populated when merchant is using sessions, or when the payment was rejected * with an object. (Ex: 'action.reject(obj)' ). Otherwise, it will be empty. * - * @param data + * @param data - session response or resultCode. It can also be undefined if payment was rejected without argument ('action.reject()') * @param element */ onPaymentFailed?(data?: SessionsResponse | { resultCode: ResultCode }, element?: UIElement): void; @@ -202,7 +203,7 @@ export interface CoreConfiguration { * @param actions */ onAdditionalDetails?( - state: any, + state: AdditionalDetailsStateData, element: UIElement, actions: { resolve: (response: CheckoutAdvancedFlowResponse) => void; diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index 09ac0cd8e4..a9b028b7b0 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -379,3 +379,15 @@ export interface ActionHandledReturnObject { componentType: string; actionDescription: ActionDescriptionType; } + +export type AdditionalDetailsStateData = { + data: { + details: { + redirectResult?: string; + threeDSResult?: string; + [key: string]: any; + }; + paymentData?: string; + sessionData?: string; + }; +}; From 135c9bc6ac3f40ae609462f1cadeed55f3c4341d Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Fri, 19 Jan 2024 13:12:30 -0300 Subject: [PATCH 34/55] fix: reverting prettier config --- .prettierrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.prettierrc.json b/.prettierrc.json index f3ca00a53b..3f1d5d07ce 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,6 +3,6 @@ "bracketSpacing": true, "trailingComma": "none", "tabWidth": 4, - "printWidth": 120, + "printWidth": 150, "singleQuote": true } From b8c03f6f5df4f3f9c09037b54033e36d5ed99b5f Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Mon, 22 Jan 2024 14:51:04 -0300 Subject: [PATCH 35/55] fixing old playground --- packages/playground/src/handlers.js | 19 +--- .../src/pages/Components/Components.js | 6 ++ .../playground/src/pages/Dropin/manual.js | 33 ++----- .../playground/src/pages/DropinAuto/manual.js | 88 ++++++++---------- .../src/pages/DropinAuto/session.js | 9 +- .../playground/src/pages/DropinUMD/manual.js | 89 ++++++++----------- .../playground/src/pages/DropinUMD/session.js | 6 +- .../src/pages/GiftCards/GiftCards.js | 13 ++- .../src/pages/OpenInvoices/OpenInvoices.js | 8 +- .../playground/src/pages/QRCodes/QRCodes.js | 58 +++++++----- .../playground/src/pages/Result/Result.js | 8 +- .../playground/src/pages/ThreeDS/ThreeDS.js | 21 +++-- .../playground/src/pages/Wallets/Wallets.js | 30 +------ 13 files changed, 172 insertions(+), 216 deletions(-) diff --git a/packages/playground/src/handlers.js b/packages/playground/src/handlers.js index 487fee13dc..38ad68e9d2 100644 --- a/packages/playground/src/handlers.js +++ b/packages/playground/src/handlers.js @@ -1,16 +1,5 @@ import { makePayment, makeDetailsCall } from './services'; -export function handleResponse(response, component) { - const type = component.data.paymentMethod ? component.data.paymentMethod.type : component.constructor.name; - console.log('\ntype=', type, 'response=', response); - - if (response.action) { - component.handleAction(response.action); - } else if (response.resultCode) { - alert(response.resultCode); - } -} - export function handleChange(state, component) { console.group(`onChange - ${state.data.paymentMethod.type}`); console.log('isValid', state.isValid); @@ -32,8 +21,6 @@ export function handleError(obj) { export async function handleSubmit(state, component, actions) { component.setStatus('loading'); - console.log('onSubmit', state, actions); - try { const { action, order, resultCode, donationToken } = await makePayment(state.data); @@ -53,9 +40,7 @@ export async function handleSubmit(state, component, actions) { export async function handleAdditionalDetails(state, component, actions) { try { - console.log('onAdditionalDetails', state, component, actions); - - const { resultCode, action, order, resultCode, donationToken } = await makeDetailsCall(state.data); + const { resultCode, action, order, donationToken } = await makeDetailsCall(state.data); if (!resultCode) actions.reject(); @@ -64,9 +49,7 @@ export async function handleAdditionalDetails(state, component, actions) { action, order, donationToken - // error: {}, }); - return; } catch (error) { console.error('## onAdditionalDetails - critical error', error); actions.reject(); diff --git a/packages/playground/src/pages/Components/Components.js b/packages/playground/src/pages/Components/Components.js index 8a93a448b4..3d3d9662c2 100644 --- a/packages/playground/src/pages/Components/Components.js +++ b/packages/playground/src/pages/Components/Components.js @@ -34,6 +34,12 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = onChange: handleChange, onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + }, onError: (error, component) => { console.info(error, component); }, diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index 134f4f02c6..82bf645d86 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -1,7 +1,6 @@ import { AdyenCheckout, Dropin, Ideal, Card, GooglePay, PayPal, Ach, Affirm, WeChat, Giftcard, AmazonPay } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; import { getPaymentMethods, makePayment, checkBalance, createOrder, cancelOrder, makeDetailsCall } from '../../services'; - import { amount, shopperLocale, countryCode } from '../../config/commonConfig'; import { getSearchParameters } from '../../utils'; import getTranslationFile from '../../config/getTranslation'; @@ -21,8 +20,6 @@ export async function initManual() { environment: process.env.__CLIENT_ENV__, onSubmit: async (state, component, actions) => { - console.log('onSubmit', state, component.authorizedEvent); - try { const { action, order, resultCode, donationToken } = await makePayment(state.data); @@ -33,33 +30,14 @@ export async function initManual() { action, order, donationToken - // error: { - // googlePayError: {}, - // applePayError: {} - // } - // } }); } catch (error) { console.error('## onSubmit - critical error', error); actions.reject(); } }, - - onChange(state, element) { - console.log('onChange', state, element); - }, - - onPaymentCompleted(result, element) { - console.log('onPaymentCompleted', result, element); - }, - onPaymentFailed(result, element) { - console.log('onPaymentFailed', result, element); - }, - onAdditionalDetails: async (state, component, actions) => { try { - console.log('onAdditionalDetails', state, component, actions); - const { resultCode, action, order, donationToken } = await makeDetailsCall(state.data); if (!resultCode) actions.reject(); @@ -69,14 +47,18 @@ export async function initManual() { action, order, donationToken - // error: {}, }); - return; } catch (error) { console.error('## onAdditionalDetails - critical error', error); actions.reject(); } }, + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + }, onBalanceCheck: async (resolve, reject, data) => { console.log('onBalanceCheck', data); resolve(await checkBalance(data)); @@ -173,9 +155,6 @@ export async function initManual() { klarna: { useKlarnaWidget: true } - // storedCard: { - // hideCVC: true - // } } }).mount('#dropin-container'); diff --git a/packages/playground/src/pages/DropinAuto/manual.js b/packages/playground/src/pages/DropinAuto/manual.js index f14138efca..379abbe798 100644 --- a/packages/playground/src/pages/DropinAuto/manual.js +++ b/packages/playground/src/pages/DropinAuto/manual.js @@ -23,39 +23,46 @@ export async function initManual() { values: [1, 2, 3, 4] } }, - onSubmit: async (state, component) => { - const result = await makePayment(state.data); - - // handle actions - if (result.action) { - // demo only - store paymentData & order - if (result.action.paymentData) localStorage.setItem('storedPaymentData', result.action.paymentData); - 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); + onSubmit: async (state, component, actions) => { + try { + const { action, order, resultCode, donationToken } = await makePayment(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); + } catch (error) { + console.error('## onSubmit - critical error', error); + actions.reject(); } }, - // onChange: state => { - // console.log('onChange', state); - // }, - onAdditionalDetails: async (state, component) => { - const result = await makeDetailsCall(state.data); - - if (result.action) { - component.handleAction(result.action); - } else { - handleFinalState(result.resultCode, component); + onAdditionalDetails: async (state, component, actions) => { + try { + const { resultCode, action, order, donationToken } = await makeDetailsCall(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); + } catch (error) { + console.error('## onAdditionalDetails - critical error', error); + actions.reject(); } }, + onPaymentCompleted(data, component) { + component.setStatus('success'); + }, + onPaymentFailed(data, component) { + component.setStatus('error'); + }, onBalanceCheck: async (resolve, reject, data) => { resolve(await checkBalance(data)); }, @@ -107,26 +114,6 @@ export async function initManual() { }); } - // Handle Amazon Pay redirect result - if (amazonCheckoutSessionId) { - window.amazonpay = new AmazonPay({ - core: checkout, - amazonCheckoutSessionId, - showOrderButton: false, - onSubmit: state => { - makePayment(state.data).then(result => { - if (result.action) { - dropin.handleAction(result.action); - } else { - handleFinalState(result.resultCode, dropin); - } - }); - } - }).mount('body'); - - window.amazonpay.submit(); - } - return Promise.resolve(true); } @@ -142,9 +129,6 @@ export async function initManual() { paywithgoogle: { buttonType: 'plain' }, - // storedCard: { - // hideCVC: true - // } klarna: { useKlarnaWidget: true } diff --git a/packages/playground/src/pages/DropinAuto/session.js b/packages/playground/src/pages/DropinAuto/session.js index 45d0fd53be..c752a3837e 100644 --- a/packages/playground/src/pages/DropinAuto/session.js +++ b/packages/playground/src/pages/DropinAuto/session.js @@ -28,8 +28,13 @@ export async function initSession() { beforeSubmit: (data, component, actions) => { actions.resolve(data); }, - onPaymentCompleted: (result, component) => { - console.info(result, component); + onPaymentCompleted(data, component) { + console.info('onPaymentCompleted', data, component); + component.setStatus('success'); + }, + onPaymentFailed(data, component) { + console.info('onPaymentFailed', data, component); + component.setStatus('error'); }, onError: (error, component) => { console.info(JSON.stringify(error), component); diff --git a/packages/playground/src/pages/DropinUMD/manual.js b/packages/playground/src/pages/DropinUMD/manual.js index 900097cf10..afffcef5dc 100644 --- a/packages/playground/src/pages/DropinUMD/manual.js +++ b/packages/playground/src/pages/DropinUMD/manual.js @@ -17,39 +17,46 @@ export async function initManual() { locale: shopperLocale, translationFile: getTranslationFile(shopperLocale), environment: process.env.__CLIENT_ENV__, - onSubmit: async (state, component) => { - const result = await makePayment(state.data); - - // handle actions - if (result.action) { - // demo only - store paymentData & order - if (result.action.paymentData) localStorage.setItem('storedPaymentData', result.action.paymentData); - 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); + onSubmit: async (state, component, actions) => { + try { + const { action, order, resultCode, donationToken } = await makePayment(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); + } catch (error) { + console.error('## onSubmit - critical error', error); + actions.reject(); } }, - // onChange: state => { - // console.log('onChange', state); - // }, - onAdditionalDetails: async (state, component) => { - const result = await makeDetailsCall(state.data); - - if (result.action) { - component.handleAction(result.action); - } else { - handleFinalState(result.resultCode, component); + onAdditionalDetails: async (state, component, actions) => { + try { + const { resultCode, action, order, donationToken } = await makeDetailsCall(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); + } catch (error) { + console.error('## onAdditionalDetails - critical error', error); + actions.reject(); } }, + onPaymentCompleted(data, component) { + component.setStatus('success'); + }, + onPaymentFailed(data, component) { + component.setStatus('error'); + }, onBalanceCheck: async (resolve, reject, data) => { resolve(await checkBalance(data)); }, @@ -100,27 +107,6 @@ export async function initManual() { return true; }); } - - // Handle Amazon Pay redirect result - if (amazonCheckoutSessionId) { - window.amazonpay = new AmazonPay({ - core: checkout, - amazonCheckoutSessionId, - showOrderButton: false, - onSubmit: state => { - makePayment(state.data).then(result => { - if (result.action) { - dropin.handleAction(result.action); - } else { - handleFinalState(result.resultCode, dropin); - } - }); - } - }).mount('body'); - - window.amazonpay.submit(); - } - return Promise.resolve(true); } @@ -136,9 +122,6 @@ export async function initManual() { paywithgoogle: { buttonType: 'plain' }, - // storedCard: { - // hideCVC: true - // } klarna: { useKlarnaWidget: true } diff --git a/packages/playground/src/pages/DropinUMD/session.js b/packages/playground/src/pages/DropinUMD/session.js index c83f4f054f..a1c211356c 100644 --- a/packages/playground/src/pages/DropinUMD/session.js +++ b/packages/playground/src/pages/DropinUMD/session.js @@ -24,12 +24,14 @@ export async function initSession() { locale: shopperLocale, translationFile: getTranslationFile(shopperLocale), - // Events beforeSubmit: (data, component, actions) => { actions.resolve(data); }, onPaymentCompleted: (result, component) => { - console.info(result, component); + console.info('onPaymentCompleted', result, component); + }, + onPaymentFailed: (result, component) => { + console.info('onPaymentFailed', result, component); }, onError: (error, component) => { console.info(JSON.stringify(error), component); diff --git a/packages/playground/src/pages/GiftCards/GiftCards.js b/packages/playground/src/pages/GiftCards/GiftCards.js index 4ea568be34..61e2931542 100644 --- a/packages/playground/src/pages/GiftCards/GiftCards.js +++ b/packages/playground/src/pages/GiftCards/GiftCards.js @@ -17,6 +17,12 @@ import getTranslationFile from '../../config/getTranslation'; environment: process.env.__CLIENT_ENV__, onChange: handleChange, onSubmit: handleSubmit, + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + }, showPayButton: true, amount }); @@ -70,8 +76,11 @@ import getTranslationFile from '../../config/getTranslation'; beforeSubmit: (data, component, actions) => { actions.resolve(data); }, - onPaymentCompleted: (result, component) => { - console.info(result, component); + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); }, onError: (error, component) => { console.error(error.message, component); diff --git a/packages/playground/src/pages/OpenInvoices/OpenInvoices.js b/packages/playground/src/pages/OpenInvoices/OpenInvoices.js index f7947a2c51..a6534a1743 100644 --- a/packages/playground/src/pages/OpenInvoices/OpenInvoices.js +++ b/packages/playground/src/pages/OpenInvoices/OpenInvoices.js @@ -28,6 +28,12 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsData => { environment: process.env.__CLIENT_ENV__, onChange: handleChange, onSubmit: handleSubmit, + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + }, onError: console.error, showPayButton: true, amount // Optional. Used to display the amount in the Pay Button. @@ -111,7 +117,7 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsData => { core: window.core, countryCode: 'US', // 'US' / 'CA' visibility: { - personalDetails: 'editable', // editable [default] / readOnly / hidden + personalDetails: 'hidden', // editable [default] / readOnly / hidden billingAddress: 'editable', deliveryAddress: 'editable' }, diff --git a/packages/playground/src/pages/QRCodes/QRCodes.js b/packages/playground/src/pages/QRCodes/QRCodes.js index 6fa7b99940..27ff0e20ea 100644 --- a/packages/playground/src/pages/QRCodes/QRCodes.js +++ b/packages/playground/src/pages/QRCodes/QRCodes.js @@ -6,22 +6,30 @@ import '../../../config/polyfills'; import '../../utils'; import '../../style.scss'; import './QRCodes.scss'; -import { handleResponse } from '../../handlers'; import getCurrency from '../../config/getCurrency'; import getTranslationFile from '../../config/getTranslation'; -const makeQRCodePayment = (state, component, countryCode) => { +const handleQRCodePayment = async (state, component, actions, countryCode) => { const currency = getCurrency(countryCode); const config = { countryCode, amount: { currency, value: 25940 } }; - return makePayment(state.data, config) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); + component.setStatus('loading'); + + try { + const { action, order, resultCode, donationToken } = await makePayment(state.data, config); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken }); + } catch (error) { + console.error('## onSubmit - critical error', error); + actions.reject(); + } }; (async () => { @@ -30,51 +38,57 @@ const makeQRCodePayment = (state, component, countryCode) => { locale: shopperLocale, translationFile: getTranslationFile(shopperLocale), environment: process.env.__CLIENT_ENV__, - risk: { node: 'body', onError: console.error } + risk: { node: 'body', onError: console.error }, + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + }, + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + } }); // WechatPay QR new WeChat({ core: checkout, type: 'wechatpayQR', - onSubmit: (state, component) => { - return makeQRCodePayment(state, component, 'CN'); + onSubmit: (state, component, actions) => { + handleQRCodePayment(state, component, actions, 'CN'); } }).mount('#wechatpayqr-container'); // BCMC Mobile new BcmcMobile({ core: checkout, - onSubmit: (state, component) => { - return makeQRCodePayment(state, component, 'BE'); + onSubmit: (state, component, actions) => { + handleQRCodePayment(state, component, actions, 'BE'); } }).mount('#bcmcqr-container'); new Swish({ core: checkout, - onSubmit: (state, component) => { - return makeQRCodePayment(state, component, 'SE'); + onSubmit: (state, component, actions) => { + handleQRCodePayment(state, component, actions, 'SE'); } }).mount('#swish-container'); new PromptPay({ core: checkout, - onSubmit: (state, component) => { - return makeQRCodePayment(state, component, 'TH'); + onSubmit: (state, component, actions) => { + handleQRCodePayment(state, component, actions, 'TH'); } }).mount('#promptpay-container'); new PayNow({ core: checkout, - onSubmit: (state, component) => { - return makeQRCodePayment(state, component, 'SG'); + onSubmit: (state, component, actions) => { + handleQRCodePayment(state, component, actions, 'SG'); } }).mount('#paynow-container'); new DuitNow({ core: checkout, - onSubmit: (state, component) => { - return makeQRCodePayment(state, component, 'MY'); + onSubmit: (state, component, actions) => { + handleQRCodePayment(state, component, actions, 'MY'); } }).mount('#duitnow-container'); })(); diff --git a/packages/playground/src/pages/Result/Result.js b/packages/playground/src/pages/Result/Result.js index 52001c524e..f5e63aecd0 100644 --- a/packages/playground/src/pages/Result/Result.js +++ b/packages/playground/src/pages/Result/Result.js @@ -10,12 +10,12 @@ async function handleRedirectResult(redirectResult, sessionId) { session: { id: sessionId }, clientKey: process.env.__CLIENT_KEY__, environment: process.env.__CLIENT_ENV__, - onPaymentCompleted: result => { - console.log('onPaymentCompleted', result); + onPaymentCompleted: (result, element) => { + console.log('onPaymentCompleted', result, element); document.querySelector('#result-container > pre').innerHTML = JSON.stringify(result, null, '\t'); }, - onPaymentFailed: result => { - console.log('onPaymentFailed', result); + onPaymentFailed: (result, element) => { + console.log('onPaymentFailed', result, element); document.querySelector('#result-container > pre').innerHTML = JSON.stringify(result, null, '\t'); }, onError: obj => { diff --git a/packages/playground/src/pages/ThreeDS/ThreeDS.js b/packages/playground/src/pages/ThreeDS/ThreeDS.js index ca8fed6587..bfdfb31819 100644 --- a/packages/playground/src/pages/ThreeDS/ThreeDS.js +++ b/packages/playground/src/pages/ThreeDS/ThreeDS.js @@ -12,13 +12,22 @@ import getTranslationFile from '../../config/getTranslation'; translationFile: getTranslationFile(shopperLocale), environment: 'test', clientKey: process.env.__CLIENT_KEY__, - onAdditionalDetails: async (state, element) => { - const result = await makeDetailsCall(state.data); - if (result.action) { - component.handleAction(result.action); - } else { - alert(result.resultCode); + onAdditionalDetails: async (state, element, actions) => { + try { + const { resultCode, action, order, donationToken } = await makeDetailsCall(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); + } catch (error) { + console.error('## onAdditionalDetails - critical error', error); + actions.reject(); } } }); diff --git a/packages/playground/src/pages/Wallets/Wallets.js b/packages/playground/src/pages/Wallets/Wallets.js index fb9c3a31d5..b526e10d8b 100644 --- a/packages/playground/src/pages/Wallets/Wallets.js +++ b/packages/playground/src/pages/Wallets/Wallets.js @@ -22,14 +22,12 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = onError(error) { console.log(error); }, - onPaymentCompleted(result, element) { console.log('onPaymentCompleted', result, element); }, onPaymentFailed(result, element) { console.log('onPaymentFailed', result, element); }, - showPayButton: true }); @@ -162,43 +160,21 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = // GOOGLE PAY const googlepay = new GooglePay({ core: window.checkout, - // environment: 'PRODUCTION', environment: 'TEST', // Callbacks onAuthorized(data, actions) { console.log('onAuthorized', data, actions); - - actions.reject(); - - // actions.reject('Failed with string'); - - // actions.reject({ - // intent: 'PAYMENT_AUTHORIZATION', - // reason: 'OTHER_ERROR', - // message: 'Failed with object' - // }); + actions.resolve(); }, - // onError: console.error, - // Payment info countryCode: 'NL', - // Merchant config (required) - // configuration: { - // gatewayMerchantId: 'TestMerchant', // name of MerchantAccount - // merchantName: 'Adyen Test merchant', // Name to be displayed - // merchantId: '06946223745213860250' // Required in Production environment. Google's merchantId: https://developers.google.com/pay/api/web/guides/test-and-deploy/deploy-production-environment#obtain-your-merchantID - // }, - // Shopper info (optional) emailRequired: true, - billingAddressRequired: true, - shippingAddressRequired: true, - // shippingAddressParameters: {}, // https://developers.google.com/pay/api/web/reference/object#ShippingAddressParameters // Button config (optional) buttonType: 'long', // https://developers.google.com/pay/api/web/reference/object#ButtonOptions @@ -222,9 +198,9 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = console.log('Apple Pay - Button clicked'); resolve(); }, - onAuthorized: (resolve, reject, event) => { + onAuthorized: (data, actions) => { console.log('Apple Pay onAuthorized', event); - resolve(); + actions.resolve(); }, buttonType: 'buy' }); From be9b5919fd744625f3cb0139282450dc3c589d66 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Tue, 23 Jan 2024 09:15:14 -0300 Subject: [PATCH 36/55] fixing storybook --- packages/lib/storybook/helpers/checkout-handlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/storybook/helpers/checkout-handlers.ts b/packages/lib/storybook/helpers/checkout-handlers.ts index fa99b2780e..00d4fcfc25 100644 --- a/packages/lib/storybook/helpers/checkout-handlers.ts +++ b/packages/lib/storybook/helpers/checkout-handlers.ts @@ -72,11 +72,11 @@ export async function handleResponse(response, component, checkout?, paymentData handleFinalState(response, component); } -export function handleChange(state: any, component: UIElement) { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function handleChange(state: any, _component: UIElement) { console.groupCollapsed(`onChange - ${state.data.paymentMethod.type}`); console.log('isValid', state.isValid); console.log('data', state.data); - console.log('node', component._node); console.log('state', state); console.groupEnd(); } From d1df1a3c81c2703f69905b34e6d7e819cd7a6acd Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Tue, 23 Jan 2024 09:18:12 -0300 Subject: [PATCH 37/55] storybook --- packages/lib/src/types/global-types.ts | 6 +- .../storybook/helpers/checkout-api-calls.ts | 19 +++-- .../helpers/create-advanced-checkout.ts | 75 +++++++++++-------- .../helpers/create-sessions-checkout.ts | 12 ++- packages/lib/storybook/stories/Container.tsx | 2 +- .../storybook/stories/cards/Card.stories.tsx | 1 - .../wallets/ApplePayExpress.stories.tsx | 42 +++++++---- .../wallets/GooglePayExpress.stories.tsx | 44 +++++++---- 8 files changed, 127 insertions(+), 74 deletions(-) diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index a9b028b7b0..91890daa18 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -369,11 +369,7 @@ export type RawPaymentResponse = PaymentResponseData & [key: string]: any; }; -export type ActionDescriptionType = - | 'qr-code-loaded' - | 'polling-started' - | 'fingerprint-iframe-loaded' - | 'challenge-iframe-loaded'; +export type ActionDescriptionType = 'qr-code-loaded' | 'polling-started' | 'fingerprint-iframe-loaded' | 'challenge-iframe-loaded'; export interface ActionHandledReturnObject { componentType: string; diff --git a/packages/lib/storybook/helpers/checkout-api-calls.ts b/packages/lib/storybook/helpers/checkout-api-calls.ts index 8254f1b17f..5bbaa9ebb5 100644 --- a/packages/lib/storybook/helpers/checkout-api-calls.ts +++ b/packages/lib/storybook/helpers/checkout-api-calls.ts @@ -1,8 +1,17 @@ import paymentMethodsConfig from '../config/paymentMethodsConfig'; import paymentsConfig from '../config/paymentsConfig'; import { httpPost } from '../utils/http-post'; -import { RawPaymentResponse } from '../../src/components/types'; -import { CheckoutSessionSetupResponse, Order, OrderStatus, PaymentAction, PaymentAmount, PaymentMethodsResponse } from '../../src/types'; +import { + Order, + OrderStatus, + PaymentAction, + PaymentAmount, + PaymentMethodsResponse, + RawPaymentResponse, + AdditionalDetailsStateData, + ResultCode +} from '../../src/types'; +import { CheckoutSessionSetupResponse } from '../../src/core/CheckoutSession/types'; export const getPaymentMethods = async (configuration?: any): Promise<PaymentMethodsResponse> => await httpPost('paymentMethods', { ...paymentMethodsConfig, ...configuration }); @@ -13,9 +22,9 @@ export const makePayment = async (stateData: any, paymentData: any): Promise<Raw return await httpPost('payments', paymentRequest); }; -export const makeDetailsCall = async (detailsData: { - details: { redirectResult: string }; -}): Promise<{ resultCode: string; action?: PaymentAction }> => await httpPost('details', detailsData); +export const makeDetailsCall = async ( + detailsData: AdditionalDetailsStateData['data'] +): Promise<{ resultCode: ResultCode; action?: PaymentAction; order?: Order; donationToken?: string }> => await httpPost('details', detailsData); export const createSession = async (data: any): Promise<CheckoutSessionSetupResponse> => { return await httpPost('sessions', { ...data, lineItems: paymentsConfig.lineItems }); diff --git a/packages/lib/storybook/helpers/create-advanced-checkout.ts b/packages/lib/storybook/helpers/create-advanced-checkout.ts index c2f3efc25f..cc7d8d0c05 100644 --- a/packages/lib/storybook/helpers/create-advanced-checkout.ts +++ b/packages/lib/storybook/helpers/create-advanced-checkout.ts @@ -1,17 +1,12 @@ import { AdyenCheckout } from '../../src/index'; -import { cancelOrder, checkBalance, createOrder, getPaymentMethods, makePayment } from './checkout-api-calls'; -import { handleAdditionalDetails, handleChange, handleError, handleFinalState } from './checkout-handlers'; +import { cancelOrder, checkBalance, createOrder, getPaymentMethods, makeDetailsCall, makePayment } from './checkout-api-calls'; +import { handleChange, handleError, handleFinalState } from './checkout-handlers'; import getCurrency from '../utils/get-currency'; import { AdyenCheckoutProps } from '../stories/types'; import Checkout from '../../src/core/core'; import { PaymentMethodsResponse } from '../../src/types'; -async function createAdvancedFlowCheckout({ - showPayButton, - countryCode, - shopperLocale, - amount -}: AdyenCheckoutProps): Promise<Checkout> { +async function createAdvancedFlowCheckout({ showPayButton, countryCode, shopperLocale, amount }: AdyenCheckoutProps): Promise<Checkout> { const paymentAmount = { currency: getCurrency(countryCode), value: Number(amount) @@ -40,34 +35,52 @@ async function createAdvancedFlowCheckout({ shopperLocale }; - const result = await makePayment(state.data, paymentData); - - // happpy flow - if (result.resultCode.includes('Refused', 'Cancelled', 'Error')) { - actions.reject({ - error: { - googlePayError: {} - } - }); - } else { - actions.resolve({ - action: result.action, - order: result.order, - resultCode: result.resultCode - }); - } + const { action, order, resultCode, donationToken } = await makePayment(state.data, paymentData); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); } catch (error) { - // Something failed in the request + console.error('## onSubmit - critical error', error); actions.reject(); } }, - onChange: (state, component) => { - handleChange(state, component); + onAdditionalDetails: async (state, component, actions) => { + try { + const { resultCode, action, order, donationToken } = await makeDetailsCall(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); + } catch (error) { + console.error('## onAdditionalDetails - critical error', error); + actions.reject(); + } + }, + + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + handleFinalState(result, element); }, - onAdditionalDetails: async (state, component) => { - await handleAdditionalDetails(state, component, checkout); + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + handleFinalState(result, element); + }, + + onChange: (state, component) => { + handleChange(state, component); }, onBalanceCheck: async (resolve, reject, data) => { @@ -103,10 +116,6 @@ async function createAdvancedFlowCheckout({ onError: (error, component) => { handleError(error, component); - }, - - onPaymentCompleted: (result, component) => { - handleFinalState(result, component); } }); diff --git a/packages/lib/storybook/helpers/create-sessions-checkout.ts b/packages/lib/storybook/helpers/create-sessions-checkout.ts index d4fcfd988e..1b2a3f5b5a 100644 --- a/packages/lib/storybook/helpers/create-sessions-checkout.ts +++ b/packages/lib/storybook/helpers/create-sessions-checkout.ts @@ -25,13 +25,19 @@ async function createSessionsCheckout({ showPayButton, countryCode, shopperLocal environment: process.env.CLIENT_ENV, session, showPayButton, - // @ts-ignore TODO: Fix beforeSubmit type + beforeSubmit: (data, component, actions) => { actions.resolve(data); }, - onPaymentCompleted: (result, component) => { - handleFinalState(result, component); + onPaymentCompleted(result, element) { + console.log('onPaymentCompleted', result, element); + handleFinalState(result, element); + }, + + onPaymentFailed(result, element) { + console.log('onPaymentFailed', result, element); + handleFinalState(result, element); }, onError: (error, component) => { diff --git a/packages/lib/storybook/stories/Container.tsx b/packages/lib/storybook/stories/Container.tsx index f0e1f4798e..ef0a635a41 100644 --- a/packages/lib/storybook/stories/Container.tsx +++ b/packages/lib/storybook/stories/Container.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'preact/hooks'; -import { IUIElement } from '../../src/components/types'; +import { IUIElement } from '../../src/components/internal/UIElement/types'; interface IContainer { element: IUIElement; diff --git a/packages/lib/storybook/stories/cards/Card.stories.tsx b/packages/lib/storybook/stories/cards/Card.stories.tsx index 78e6060738..72164d8302 100644 --- a/packages/lib/storybook/stories/cards/Card.stories.tsx +++ b/packages/lib/storybook/stories/cards/Card.stories.tsx @@ -64,7 +64,6 @@ export const WithInstallments: CardStory = { args: { componentConfiguration: { _disableClickToPay: true, - showBrandsUnderCardNumber: true, showInstallmentAmounts: true, installmentOptions: { mc: { diff --git a/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx b/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx index 3687e3d160..5715049d46 100644 --- a/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx +++ b/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx @@ -2,9 +2,9 @@ import { MetaConfiguration, PaymentMethodStoryProps, StoryConfiguration } from ' import { getStoryContextCheckout } from '../../utils/get-story-context-checkout'; import { Container } from '../Container'; import { ApplePayConfiguration } from '../../../src/components/ApplePay/types'; -import { handleSubmit } from '../../helpers/checkout-handlers'; import getCurrency from '../../utils/get-currency'; import { ApplePay } from '../../../src'; +import { makePayment } from '../../helpers/checkout-api-calls'; type ApplePayStory = StoryConfiguration<ApplePayConfiguration>; @@ -154,20 +154,36 @@ export const Express: ApplePayStory = { componentConfiguration: { countryCode: COUNTRY_CODE, - onSubmit: (state, component) => { - const paymentData = { - amount: { - currency: getCurrency(COUNTRY_CODE), - value: ApplePayAmountHelper.getFinalAdyenAmount() - }, - countryCode: COUNTRY_CODE, - shopperLocale: SHOPPER_LOCALE - }; - handleSubmit(state, component, null, paymentData); + onSubmit: async (state, component, actions) => { + try { + const paymentData = { + amount: { + currency: getCurrency(COUNTRY_CODE), + value: ApplePayAmountHelper.getFinalAdyenAmount() + }, + countryCode: COUNTRY_CODE, + shopperLocale: SHOPPER_LOCALE + }; + + const { action, order, resultCode, donationToken } = await makePayment(state.data, paymentData); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); + } catch (error) { + console.error('## onSubmit - critical error', error); + actions.reject(); + } }, - onAuthorized: paymentData => { - console.log('Shopper details', paymentData); + onAuthorized: (data, actions) => { + console.log('Authorized event', data); + actions.resolve(); }, onShippingContactSelected: async (resolve, reject, event) => { diff --git a/packages/lib/storybook/stories/wallets/GooglePayExpress.stories.tsx b/packages/lib/storybook/stories/wallets/GooglePayExpress.stories.tsx index 0a61a09004..03226ecdc2 100644 --- a/packages/lib/storybook/stories/wallets/GooglePayExpress.stories.tsx +++ b/packages/lib/storybook/stories/wallets/GooglePayExpress.stories.tsx @@ -2,9 +2,9 @@ import { MetaConfiguration, PaymentMethodStoryProps, StoryConfiguration } from ' import { getStoryContextCheckout } from '../../utils/get-story-context-checkout'; import { Container } from '../Container'; import { GooglePayConfiguration } from '../../../src/components/GooglePay/types'; -import { handleSubmit } from '../../helpers/checkout-handlers'; import getCurrency from '../../utils/get-currency'; import { GooglePay } from '../../../src'; +import { makePayment } from '../../helpers/checkout-api-calls'; type GooglePayStory = StoryConfiguration<GooglePayConfiguration>; @@ -171,20 +171,38 @@ export const Express: GooglePayStory = { amount: INITIAL_AMOUNT, shopperLocale: SHOPPER_LOCALE, componentConfiguration: { - onSubmit: (state, component) => { - const paymentData = { - amount: { - currency: getCurrency(COUNTRY_CODE), - value: finalAmount - }, - countryCode: COUNTRY_CODE, - shopperLocale: SHOPPER_LOCALE - }; - handleSubmit(state, component, null, paymentData); + onSubmit: async (state, component, actions) => { + try { + const paymentData = { + amount: { + currency: getCurrency(COUNTRY_CODE), + value: finalAmount + }, + countryCode: COUNTRY_CODE, + shopperLocale: SHOPPER_LOCALE + }; + + const { action, order, resultCode, donationToken } = await makePayment(state.data, paymentData); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken + }); + } catch (error) { + console.error('## onSubmit - critical error', error); + actions.reject(); + } }, - onAuthorized: paymentData => { - console.log('Shopper details', paymentData); + + onAuthorized: (data, actions) => { + console.log('Authorized data', data); + actions.resolve(); }, + transactionInfo: getTransactionInfo(), callbackIntents: ['SHIPPING_ADDRESS', 'SHIPPING_OPTION'], From 20b273f8a62ec95fb2680a223f92601013e91b34 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Tue, 23 Jan 2024 09:40:17 -0300 Subject: [PATCH 38/55] fix googletest --- .../components/GooglePay/GooglePay.test.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/lib/src/components/GooglePay/GooglePay.test.ts b/packages/lib/src/components/GooglePay/GooglePay.test.ts index a5e3fd83d5..4e42386dc0 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.test.ts +++ b/packages/lib/src/components/GooglePay/GooglePay.test.ts @@ -97,16 +97,16 @@ describe('GooglePay', () => { postalCode: '94043', street: '' }); - expect(state.data.browserInfo).toStrictEqual({ - acceptHeader: '*/*', - colorDepth: 24, - javaEnabled: false, - language: 'en-US', - screenHeight: '', - screenWidth: '', - timeZoneOffset: 360, - userAgent: 'Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/20.0.3' - }); + + const browserInfo = state.data.browserInfo; + + expect(browserInfo.colorDepth).toEqual(expect.any(Number)); + expect(browserInfo.javaEnabled).toEqual(expect.any(Boolean)); + expect(browserInfo.language).toEqual(expect.any(String)); + expect(browserInfo.screenHeight).toEqual(''); + expect(browserInfo.screenWidth).toEqual(''); + expect(browserInfo.timeZoneOffset).toEqual(expect.any(Number)); + expect(browserInfo.userAgent).toEqual(expect.any(String)); await new Promise(process.nextTick); @@ -184,10 +184,7 @@ describe('GooglePay', () => { transactionState: 'ERROR' }); - expect(onPaymentFailedMock).toHaveBeenCalledWith( - { resultCode: 'Refused', error: { googlePayError: 'Insufficient funds' } }, - gpay - ); + expect(onPaymentFailedMock).toHaveBeenCalledWith({ resultCode: 'Refused', error: { googlePayError: 'Insufficient funds' } }, gpay); }); }); From 0aaa38916277ddd3ca91fdeac39413a5dce1d800 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Tue, 23 Jan 2024 10:56:53 -0300 Subject: [PATCH 39/55] fix: updating e2e playwright --- packages/e2e-playwright/app/src/handlers.js | 64 ++++++++++--------- .../e2e-playwright/app/src/pages/ANCV/ANCV.js | 3 +- .../app/src/pages/Cards/Cards.js | 3 +- .../app/src/pages/CustomCards/CustomCards.js | 18 ++++-- .../app/src/pages/Dropin/Dropin.js | 3 +- .../app/src/pages/IssuerLists/IssuerLists.js | 3 +- 6 files changed, 55 insertions(+), 39 deletions(-) diff --git a/packages/e2e-playwright/app/src/handlers.js b/packages/e2e-playwright/app/src/handlers.js index 6020ee05f6..15c1f15a87 100644 --- a/packages/e2e-playwright/app/src/handlers.js +++ b/packages/e2e-playwright/app/src/handlers.js @@ -1,24 +1,11 @@ import { makePayment, makeDetailsCall, createOrder } from './services'; -function removeComponent(component) { - component.remove(); -} - export function showAuthorised(message = 'Authorised') { const resultElement = document.getElementById('result-message'); resultElement.classList.remove('hide'); resultElement.innerText = message; } -export function handleResponse(response, component) { - if (response.action) { - component.handleAction(response.action, window.actionConfigObject || {}); - } else if (response.resultCode) { - component.remove(); - showAuthorised(); - } -} - export function handleError(obj) { // SecuredField related errors should not go straight to console.error if (obj.type === 'card') { @@ -28,28 +15,45 @@ export function handleError(obj) { } } -export function handleSubmit(state, component) { - component.setStatus('loading'); +export async function handleSubmit(state, component, actions) { + try { + const { action, order, resultCode, donationToken } = await makePayment(state.data); - return makePayment(state.data) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken }); + } catch (error) { + console.error('## onSubmit - critical error', error); + actions.reject(); + } } -export function handleAdditionalDetails(details, component) { - return makeDetailsCall(details.data) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); +export async function handleAdditionalDetails(state, component, actions) { + try { + const { resultCode, action, order, donationToken } = await makeDetailsCall(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken }); + } catch (error) { + console.error('## onAdditionalDetails - critical error', error); + actions.reject(); + } +} + +export function handlePaymentCompleted(data, component) { + component.remove(); + showAuthorised(); } export function handleOrderRequest(resolve, reject, data) { diff --git a/packages/e2e-playwright/app/src/pages/ANCV/ANCV.js b/packages/e2e-playwright/app/src/pages/ANCV/ANCV.js index 5c0c7df19e..878b63663b 100644 --- a/packages/e2e-playwright/app/src/pages/ANCV/ANCV.js +++ b/packages/e2e-playwright/app/src/pages/ANCV/ANCV.js @@ -1,6 +1,6 @@ import { AdyenCheckout, ANCV } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleError, showAuthorised } from '../../handlers'; +import { handleError, handlePaymentCompleted, showAuthorised } from '../../handlers'; import { shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; import { createSession } from '../../services'; @@ -27,6 +27,7 @@ const initCheckout = async () => { locale: shopperLocale, countryCode, showPayButton: true, + onPaymentCompleted: handlePaymentCompleted, onOrderUpdated: data => { showAuthorised('Partially Authorised'); }, diff --git a/packages/e2e-playwright/app/src/pages/Cards/Cards.js b/packages/e2e-playwright/app/src/pages/Cards/Cards.js index d6c5777ab7..09feb6fd5e 100644 --- a/packages/e2e-playwright/app/src/pages/Cards/Cards.js +++ b/packages/e2e-playwright/app/src/pages/Cards/Cards.js @@ -1,6 +1,6 @@ import { AdyenCheckout, Card } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; @@ -15,6 +15,7 @@ const initCheckout = async () => { showPayButton: true, onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError, ...window.mainConfiguration }); diff --git a/packages/e2e-playwright/app/src/pages/CustomCards/CustomCards.js b/packages/e2e-playwright/app/src/pages/CustomCards/CustomCards.js index 1366581bdc..9248945ebe 100644 --- a/packages/e2e-playwright/app/src/pages/CustomCards/CustomCards.js +++ b/packages/e2e-playwright/app/src/pages/CustomCards/CustomCards.js @@ -1,10 +1,11 @@ import { AdyenCheckout, CustomCard } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleSubmit, handleAdditionalDetails } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handlePaymentCompleted, showAuthorised } from '../../handlers'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; import './customcards.style.scss'; import { setFocus, onBrand, onConfigSuccess, onBinLookup, onChange } from './customCards.config'; +import { makePayment } from '../../services'; const initCheckout = async () => { // window.TextEncoder = null; // Comment in to force use of "compat" version @@ -15,8 +16,7 @@ const initCheckout = async () => { countryCode, environment: 'test', showPayButton: true, - onSubmit: handleSubmit, - onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, ...window.mainConfiguration }); @@ -56,7 +56,7 @@ const initCheckout = async () => { payBtn.setAttribute('data-testid', `pay-${attribute}`); payBtn.classList.add('adyen-checkout__button', 'js-components-button--one-click', `js-pay-${attribute}`); - payBtn.addEventListener('click', e => { + payBtn.addEventListener('click', async e => { e.preventDefault(); console.log('### CustomCards::createPayButton:: click attribut', attribute); @@ -70,7 +70,15 @@ const initCheckout = async () => { }; component.state.data = { paymentMethod }; - handleSubmit(component.state, component); + const response = await makePayment(component.state.data); + component.setStatus('ready'); + + if (response.action) { + component.handleAction(response.action, window.actionConfigObject || {}); + } else if (response.resultCode) { + component.remove(); + showAuthorised(); + } }); document.querySelector(parent).appendChild(payBtn); diff --git a/packages/e2e-playwright/app/src/pages/Dropin/Dropin.js b/packages/e2e-playwright/app/src/pages/Dropin/Dropin.js index b57600d2a1..20d3da7869 100644 --- a/packages/e2e-playwright/app/src/pages/Dropin/Dropin.js +++ b/packages/e2e-playwright/app/src/pages/Dropin/Dropin.js @@ -2,7 +2,7 @@ import { AdyenCheckout, Dropin } from '@adyen/adyen-web/auto'; import '@adyen/adyen-web/styles/adyen.css'; import { getPaymentMethods } from '../../services'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import '../../style.scss'; const initCheckout = async () => { @@ -19,6 +19,7 @@ const initCheckout = async () => { environment: 'test', onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError, ...window.mainConfiguration }); diff --git a/packages/e2e-playwright/app/src/pages/IssuerLists/IssuerLists.js b/packages/e2e-playwright/app/src/pages/IssuerLists/IssuerLists.js index 7886d24b1f..c94c3ba257 100644 --- a/packages/e2e-playwright/app/src/pages/IssuerLists/IssuerLists.js +++ b/packages/e2e-playwright/app/src/pages/IssuerLists/IssuerLists.js @@ -1,6 +1,6 @@ import { AdyenCheckout, Ideal } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; import { getPaymentMethods } from '../../services'; @@ -24,6 +24,7 @@ const initCheckout = async () => { showPayButton: true, onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError // ...window.mainConfiguration }); From cd8561da6eb3240006030b6b601c7db99aa0da1a Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Tue, 23 Jan 2024 11:04:02 -0300 Subject: [PATCH 40/55] updating size-limit --- packages/lib/.size-limit.cjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/lib/.size-limit.cjs b/packages/lib/.size-limit.cjs index e899827a15..72647072d3 100644 --- a/packages/lib/.size-limit.cjs +++ b/packages/lib/.size-limit.cjs @@ -5,7 +5,7 @@ module.exports = [ { name: 'UMD', path: 'dist/umd/adyen.js', - limit: '215 KB', + limit: '230 KB', running: false, }, /** @@ -15,7 +15,7 @@ module.exports = [ name: 'Auto', path: 'auto/auto.js', import: "{ AdyenCheckout, Dropin }", - limit: '130 KB', + limit: '135 KB', running: false, }, /** @@ -32,21 +32,21 @@ module.exports = [ name: 'ESM - Core + Card', path: 'dist/es/index.js', import: "{ AdyenCheckout, Card }", - limit: '70 KB', + limit: '75 KB', running: false, }, { name: 'ESM - Core + Dropin with Card and Ideal', path: 'dist/es/index.js', import: "{ AdyenCheckout, Dropin, Card, Ideal }", - limit: '75 KB', + limit: '80 KB', running: false, }, { name: 'ESM - Core + Dropin with Card and multiple languages', path: 'dist/es/index.js', import: "{ AdyenCheckout, Dropin, Card, pt_BR, nl_NL, es_ES }", - limit: '90 KB', + limit: '95 KB', running: false, }, ] From c198dd922414565281c04b1fc59db719c2d748b9 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Tue, 23 Jan 2024 16:25:33 -0300 Subject: [PATCH 41/55] e2e testcafe: updating files --- packages/e2e/app/src/handlers.js | 53 +++++++++++------ packages/e2e/app/src/pages/Cards/Cards.js | 48 ++++++++-------- .../app/src/pages/CustomCards/CustomCards.js | 57 +++++++++++-------- packages/e2e/app/src/pages/Dropin/Dropin.js | 5 +- .../e2e/app/src/pages/GiftCards/GiftCards.js | 24 ++++---- .../app/src/pages/IssuerLists/IssuerLists.js | 3 +- .../src/pages/OpenInvoices/OpenInvoices.js | 5 +- .../app/src/pages/StoredCards/StoredCards.js | 5 +- packages/e2e/app/src/utils/handlers.js | 37 ------------ .../onOrderUpdated/onOrderUpdated.test.js | 2 +- 10 files changed, 115 insertions(+), 124 deletions(-) delete mode 100644 packages/e2e/app/src/utils/handlers.js diff --git a/packages/e2e/app/src/handlers.js b/packages/e2e/app/src/handlers.js index fdea9a217d..26a7b5d317 100644 --- a/packages/e2e/app/src/handlers.js +++ b/packages/e2e/app/src/handlers.js @@ -17,26 +17,43 @@ export function handleError(obj) { } } -export function handleSubmit(state, component) { - component.setStatus('loading'); - - return makePayment(state.data) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); +export async function handleSubmit(state, component, actions) { + try { + const { action, order, resultCode, donationToken } = await makePayment(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken }); + } catch (error) { + console.error('## onSubmit - critical error', error); + actions.reject(); + } } -export function handleAdditionalDetails(details, component) { - return makeDetailsCall(details.data) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); +export async function handleAdditionalDetails(state, component, actions) { + try { + const { resultCode, action, order, donationToken } = await makeDetailsCall(state.data); + + if (!resultCode) actions.reject(); + + actions.resolve({ + resultCode, + action, + order, + donationToken }); + } catch (error) { + console.error('## onAdditionalDetails - critical error', error); + actions.reject(); + } +} + +export function handlePaymentCompleted(data, component) { + component.remove(); + alert(data.resultCode); } diff --git a/packages/e2e/app/src/pages/Cards/Cards.js b/packages/e2e/app/src/pages/Cards/Cards.js index e5216eff92..09feb6fd5e 100644 --- a/packages/e2e/app/src/pages/Cards/Cards.js +++ b/packages/e2e/app/src/pages/Cards/Cards.js @@ -1,6 +1,6 @@ import { AdyenCheckout, Card } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; @@ -15,35 +15,35 @@ const initCheckout = async () => { showPayButton: true, onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError, ...window.mainConfiguration }); // Credit card with installments window.card = new Card({ - core: checkout, - brands: ['mc', 'visa', 'amex', 'maestro', 'bcmc'], - onChange: state => { - /** - * Needed now that, for v5, we enhance the securedFields state.errors object with a rootNode prop - * - Testcafe doesn't like a ClientFunction retrieving an object with a DOM node in it!? - * - * AND, for some reason, if you place this onChange function in expiryDate.clientScripts.js it doesn't always get read. - * It'll work when it's part of a small batch but if part of the full test suite it gets ignored - so the tests that rely on - * window.mappedStateErrors fail - */ - if (!!Object.keys(state.errors).length) { - // Replace any rootNode values in the objects in state.errors with an empty string - const nuErrors = Object.entries(state.errors).reduce((acc, [fieldType, error]) => { - acc[fieldType] = error ? { ...error, rootNode: '' } : error; - return acc; - }, {}); - window.mappedStateErrors = nuErrors; - } - }, - ...window.cardConfig - }) - .mount('.card-field'); + core: checkout, + brands: ['mc', 'visa', 'amex', 'maestro', 'bcmc'], + onChange: state => { + /** + * Needed now that, for v5, we enhance the securedFields state.errors object with a rootNode prop + * - Testcafe doesn't like a ClientFunction retrieving an object with a DOM node in it!? + * + * AND, for some reason, if you place this onChange function in expiryDate.clientScripts.js it doesn't always get read. + * It'll work when it's part of a small batch but if part of the full test suite it gets ignored - so the tests that rely on + * window.mappedStateErrors fail + */ + if (!!Object.keys(state.errors).length) { + // Replace any rootNode values in the objects in state.errors with an empty string + const nuErrors = Object.entries(state.errors).reduce((acc, [fieldType, error]) => { + acc[fieldType] = error ? { ...error, rootNode: '' } : error; + return acc; + }, {}); + window.mappedStateErrors = nuErrors; + } + }, + ...window.cardConfig + }).mount('.card-field'); }; initCheckout(); diff --git a/packages/e2e/app/src/pages/CustomCards/CustomCards.js b/packages/e2e/app/src/pages/CustomCards/CustomCards.js index ee021aee4a..a3a26f274f 100644 --- a/packages/e2e/app/src/pages/CustomCards/CustomCards.js +++ b/packages/e2e/app/src/pages/CustomCards/CustomCards.js @@ -1,10 +1,12 @@ -import { AdyenCheckout, CustomCard} from '@adyen/adyen-web'; +import { AdyenCheckout, CustomCard } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; import { handleSubmit, handleAdditionalDetails } from '../../handlers'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; import './customcards.style.scss'; import { setFocus, onBrand, onConfigSuccess, onBinLookup, onChange } from './customCards.config'; +import { makePayment } from '@adyen/adyen-web-playwright/app/src/services'; +import { showAuthorised } from '@adyen/adyen-web-playwright/app/src/handlers'; const initCheckout = async () => { // window.TextEncoder = null; // Comment in to force use of "compat" version @@ -21,32 +23,30 @@ const initCheckout = async () => { }); window.securedFields = new CustomCard({ - core: checkout, - type: 'card', - brands: ['mc', 'visa', 'amex', 'bcmc', 'maestro', 'cartebancaire'], - onConfigSuccess, - onBrand, - onFocus: setFocus, - onBinLookup, - onChange, - ...window.cardConfig - }) - .mount('.secured-fields'); + core: checkout, + type: 'card', + brands: ['mc', 'visa', 'amex', 'bcmc', 'maestro', 'cartebancaire'], + onConfigSuccess, + onBrand, + onFocus: setFocus, + onBinLookup, + onChange, + ...window.cardConfig + }).mount('.secured-fields'); createPayButton('.secured-fields', window.securedFields, 'securedfields'); window.securedFields2 = new CustomCard({ - core: checkout, - // type: 'card',// Deliberately exclude to ensure a default value is set - brands: ['mc', 'visa', 'amex', 'bcmc', 'maestro', 'cartebancaire'], - onConfigSuccess, - onBrand, - onFocus: setFocus, - onBinLookup, - onChange, - ...window.cardConfig - }) - .mount('.secured-fields-2'); + core: checkout, + // type: 'card',// Deliberately exclude to ensure a default value is set + brands: ['mc', 'visa', 'amex', 'bcmc', 'maestro', 'cartebancaire'], + onConfigSuccess, + onBrand, + onFocus: setFocus, + onBinLookup, + onChange, + ...window.cardConfig + }).mount('.secured-fields-2'); createPayButton('.secured-fields-2', window.securedFields2, 'securedfields2'); @@ -57,7 +57,7 @@ const initCheckout = async () => { payBtn.name = 'pay'; payBtn.classList.add('adyen-checkout__button', 'js-components-button--one-click', `js-${attribute}`); - payBtn.addEventListener('click', e => { + payBtn.addEventListener('click', async e => { e.preventDefault(); if (!component.isValid) return component.showValidation(); @@ -69,7 +69,14 @@ const initCheckout = async () => { }; component.state.data = { paymentMethod }; - handleSubmit(component.state, component); + const response = await makePayment(component.state.data); + component.setStatus('ready'); + + if (response.action) { + component.handleAction(response.action, window.actionConfigObject || {}); + } else if (response.resultCode) { + alert(response.resultCode); + } }); document.querySelector(parent).appendChild(payBtn); diff --git a/packages/e2e/app/src/pages/Dropin/Dropin.js b/packages/e2e/app/src/pages/Dropin/Dropin.js index 6550046278..64a201424e 100644 --- a/packages/e2e/app/src/pages/Dropin/Dropin.js +++ b/packages/e2e/app/src/pages/Dropin/Dropin.js @@ -2,7 +2,7 @@ import { AdyenCheckout, Dropin } from '@adyen/adyen-web/auto'; import '@adyen/adyen-web/styles/adyen.css'; import { getPaymentMethods } from '../../services'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import '../../style.scss'; const initCheckout = async () => { @@ -17,11 +17,12 @@ const initCheckout = async () => { environment: 'test', onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError, ...window.mainConfiguration }); - window.dropin = new Dropin({core: checkout, ...window.dropinConfig}).mount('#dropin-container'); + window.dropin = new Dropin({ core: checkout, ...window.dropinConfig }).mount('#dropin-container'); }; initCheckout(); diff --git a/packages/e2e/app/src/pages/GiftCards/GiftCards.js b/packages/e2e/app/src/pages/GiftCards/GiftCards.js index baa3f7cc2d..bd261c5c66 100644 --- a/packages/e2e/app/src/pages/GiftCards/GiftCards.js +++ b/packages/e2e/app/src/pages/GiftCards/GiftCards.js @@ -1,6 +1,6 @@ import { AdyenCheckout, Giftcard } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import { checkBalance, createOrder } from '../../services'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; @@ -15,22 +15,22 @@ const initCheckout = async () => { showPayButton: true, onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError, ...window.mainConfiguration }); window.giftcard = new Giftcard({ - core: window.checkout, - type: 'giftcard', - brand: 'valuelink', - onBalanceCheck: async (resolve, reject, data) => { - resolve(await checkBalance(data)); - }, - onOrderRequest: async (resolve, reject) => { - resolve(await createOrder({ amount })); - } - }) - .mount('.card-field'); + core: window.checkout, + type: 'giftcard', + brand: 'valuelink', + onBalanceCheck: async (resolve, reject, data) => { + resolve(await checkBalance(data)); + }, + onOrderRequest: async (resolve, reject) => { + resolve(await createOrder({ amount })); + } + }).mount('.card-field'); }; initCheckout(); diff --git a/packages/e2e/app/src/pages/IssuerLists/IssuerLists.js b/packages/e2e/app/src/pages/IssuerLists/IssuerLists.js index 7886d24b1f..c94c3ba257 100644 --- a/packages/e2e/app/src/pages/IssuerLists/IssuerLists.js +++ b/packages/e2e/app/src/pages/IssuerLists/IssuerLists.js @@ -1,6 +1,6 @@ import { AdyenCheckout, Ideal } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; import { getPaymentMethods } from '../../services'; @@ -24,6 +24,7 @@ const initCheckout = async () => { showPayButton: true, onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError // ...window.mainConfiguration }); diff --git a/packages/e2e/app/src/pages/OpenInvoices/OpenInvoices.js b/packages/e2e/app/src/pages/OpenInvoices/OpenInvoices.js index 090528f955..a0a1fe25a4 100644 --- a/packages/e2e/app/src/pages/OpenInvoices/OpenInvoices.js +++ b/packages/e2e/app/src/pages/OpenInvoices/OpenInvoices.js @@ -1,6 +1,6 @@ import { AdyenCheckout, AfterPay } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; import { getPaymentMethods } from '../../services'; @@ -21,11 +21,12 @@ const initCheckout = async () => { showPayButton: true, onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError // ...window.mainConfiguration }); - window.afterpay = new AfterPay({core: checkout}).mount('.afterpay-field'); + window.afterpay = new AfterPay({ core: checkout }).mount('.afterpay-field'); }; initCheckout(); diff --git a/packages/e2e/app/src/pages/StoredCards/StoredCards.js b/packages/e2e/app/src/pages/StoredCards/StoredCards.js index 6698e203ef..bec33e2e4a 100644 --- a/packages/e2e/app/src/pages/StoredCards/StoredCards.js +++ b/packages/e2e/app/src/pages/StoredCards/StoredCards.js @@ -1,6 +1,6 @@ import { AdyenCheckout, Card } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; -import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers'; +import { handleSubmit, handleAdditionalDetails, handleError, handlePaymentCompleted } from '../../handlers'; import { amount, shopperLocale, countryCode } from '../../services/commonConfig'; import '../../style.scss'; @@ -14,6 +14,7 @@ const initCheckout = async () => { showPayButton: true, onSubmit: handleSubmit, onAdditionalDetails: handleAdditionalDetails, + onPaymentCompleted: handlePaymentCompleted, onError: handleError, ...window.mainConfiguration }); @@ -34,7 +35,7 @@ const initCheckout = async () => { }; // Credit card with installments - window.storedCard = new Card({ core: checkout, ...storedCardData}).mount('.stored-card-field'); + window.storedCard = new Card({ core: checkout, ...storedCardData }).mount('.stored-card-field'); }; initCheckout(); diff --git a/packages/e2e/app/src/utils/handlers.js b/packages/e2e/app/src/utils/handlers.js deleted file mode 100644 index 35b8c29abe..0000000000 --- a/packages/e2e/app/src/utils/handlers.js +++ /dev/null @@ -1,37 +0,0 @@ -import { makePayment, makeDetailsCall } from '../services'; - -export function handleResponse(response, component) { - if (response.action) { - component.handleAction(response.action); - } else if (response.resultCode) { - alert(response.resultCode); - } -} - -export function handleError(obj) { - console.error(obj); -} - -export function handleSubmit(state, component) { - component.setStatus('loading'); - - return makePayment(state.data) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); - }); -} - -export function handleAdditionalDetails(details, component) { - return makeDetailsCall(details.data) - .then(response => { - component.setStatus('ready'); - handleResponse(response, component); - }) - .catch(error => { - throw Error(error); - }); -} diff --git a/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js b/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js index 02734399e3..9227ff7dd2 100644 --- a/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js +++ b/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js @@ -3,7 +3,7 @@ import { ClientFunction } from 'testcafe'; import { fillIFrame, getInputSelector } from '../../utils/commonUtils'; import { GIFTCARD_NUMBER, GIFTCARD_PIN } from '../utils/constants'; import { GIFTCARDS_SESSIONS_URL } from '../../pages'; -import { mock, noCallbackMock, loggers, MOCK_SESSION_DATA } from './onOrderCreated.mocks'; +import { mock, noCallbackMock, loggers, MOCK_SESSION_DATA } from './onOrderUpdated.mocks'; import { GiftCardSessionPage } from '../../_models/GiftCardComponent.page'; From 0c5319f185c21056922e9b5f9bb55acbf856e92e Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Wed, 24 Jan 2024 15:44:00 -0300 Subject: [PATCH 42/55] fixed testcafe tests --- .../pages/DropinSessions/DropinSessions.js | 4 ---- packages/e2e/app/src/services/index.js | 4 ++++ .../e2e/tests/bacsDD/bacs.clientScripts.js | 10 +++++----- packages/e2e/tests/bacsDD/bacs.test.js | 2 +- .../cards/Bancontact/bancontact.visa.test.js | 2 +- .../binLookup.branding.reset.test.js | 4 ++-- .../dualBranding/dualBranding.dropin.test.js | 6 +++--- .../startWithoutKCP/startWithoutKCP.test.js | 16 +++++---------- .../threeDS2/threeDS2.default.size.test.js | 7 ++----- .../threeDS2.handleAction.clientScripts.js | 20 ++++++++----------- .../threeDS2/threeDS2.handleAction.test.js | 16 +++++++-------- .../cards/threeDS2/threeDS2.redirect.test.js | 2 +- .../e2e/tests/cards/threeDS2/threeDS2.test.js | 2 +- .../dropinSessions/dropinSessions.test.js | 2 +- .../onOrderUpdated/onOrderUpdated.test.js | 2 +- packages/e2e/tests/storedCard/general.test.js | 8 ++++---- .../Dropin/components/DropinComponent.tsx | 3 ++- 17 files changed, 49 insertions(+), 61 deletions(-) diff --git a/packages/e2e/app/src/pages/DropinSessions/DropinSessions.js b/packages/e2e/app/src/pages/DropinSessions/DropinSessions.js index 6863bd18ad..5036a60c32 100644 --- a/packages/e2e/app/src/pages/DropinSessions/DropinSessions.js +++ b/packages/e2e/app/src/pages/DropinSessions/DropinSessions.js @@ -19,10 +19,6 @@ const initCheckout = async () => { clientKey: process.env.__CLIENT_KEY__, session, - // Events - beforeSubmit: (data, component, actions) => { - actions.resolve(data); - }, onPaymentCompleted: (result, component) => { console.info(result, component); }, diff --git a/packages/e2e/app/src/services/index.js b/packages/e2e/app/src/services/index.js index 4beb8268a5..92ccaed05a 100644 --- a/packages/e2e/app/src/services/index.js +++ b/packages/e2e/app/src/services/index.js @@ -20,6 +20,10 @@ export const getPaymentMethods = configuration => .catch(console.error); export const makePayment = (data, config = {}) => { + if (data.paymentMethod.storedPaymentMethodId) { + config = { recurringProcessingModel: 'CardOnFile', ...config }; + } + // NOTE: Merging data object. DO NOT do this in production. const paymentRequest = { ...paymentsConfig, ...config, ...data }; return httpPost('payments', paymentRequest) diff --git a/packages/e2e/tests/bacsDD/bacs.clientScripts.js b/packages/e2e/tests/bacsDD/bacs.clientScripts.js index f9291baf91..dce14d1e3e 100644 --- a/packages/e2e/tests/bacsDD/bacs.clientScripts.js +++ b/packages/e2e/tests/bacsDD/bacs.clientScripts.js @@ -1,12 +1,12 @@ window.dropinConfig = { - showStoredPaymentMethods: false // hide stored PMs -}; - -window.mainConfiguration = { - removePaymentMethods: ['paywithgoogle', 'applepay'], + showStoredPaymentMethods: false, paymentMethodsConfiguration: { card: { _disableClickToPay: true } } }; + +window.mainConfiguration = { + removePaymentMethods: ['paywithgoogle', 'applepay'] +}; diff --git a/packages/e2e/tests/bacsDD/bacs.test.js b/packages/e2e/tests/bacsDD/bacs.test.js index 573906625b..51ba2ad39d 100644 --- a/packages/e2e/tests/bacsDD/bacs.test.js +++ b/packages/e2e/tests/bacsDD/bacs.test.js @@ -15,7 +15,7 @@ const validTicks = Selector('.adyen-checkout-input__inline-validation--valid'); const voucher = Selector('.adyen-checkout__voucher-result--directdebit_GB'); const voucherButton = Selector('.adyen-checkout__voucher-result--directdebit_GB .adyen-checkout__button--action'); -const TEST_SPEED = 1; +const TEST_SPEED = 0.8; fixture`Testing BacsDD in dropin`.page(BASE_URL + '?countryCode=GB').clientScripts('bacs.clientScripts.js'); diff --git a/packages/e2e/tests/cards/Bancontact/bancontact.visa.test.js b/packages/e2e/tests/cards/Bancontact/bancontact.visa.test.js index 3dafe7d7b6..8cf9fd4861 100644 --- a/packages/e2e/tests/cards/Bancontact/bancontact.visa.test.js +++ b/packages/e2e/tests/cards/Bancontact/bancontact.visa.test.js @@ -68,7 +68,7 @@ test('#3 Enter card number, that we mock to co-branded bcmc/visa ' + 'then compl await dropinPage.cc.cardUtils.fillDate(t, TEST_DATE_VALUE); // Expect comp to now be valid - await t.expect(dropinPage.getFromWindow('dropin.isValid')).eql(true); + await t.expect(dropinPage.getFromWindow('dropin.isValid')).eql(true, { timeout: 3000 }); }); test( diff --git a/packages/e2e/tests/cards/dualBranding/binLookup.branding.reset.test.js b/packages/e2e/tests/cards/dualBranding/binLookup.branding.reset.test.js index 3df8fbe5bf..c38a4d4379 100644 --- a/packages/e2e/tests/cards/dualBranding/binLookup.branding.reset.test.js +++ b/packages/e2e/tests/cards/dualBranding/binLookup.branding.reset.test.js @@ -7,7 +7,7 @@ import { BCMC_CARD, UNKNOWN_VISA_CARD, REGULAR_TEST_CARD } from '../utils/consta const cvcSpan = Selector('.adyen-checkout__dropin .adyen-checkout__field__cvc'); const brandingIcon = Selector('.adyen-checkout__dropin .adyen-checkout__card__cardNumber__brandIcon'); -const dualBrandingIconHolderActive = Selector('.adyen-checkout__payment-method--card .adyen-checkout__card__dual-branding__buttons--active'); +const dualBrandingIconHolderActive = Selector('.adyen-checkout__payment-method--scheme .adyen-checkout__card__dual-branding__buttons--active'); const getPropFromPMData = ClientFunction(prop => { return window.dropin.dropinRef.state.activePaymentMethod.formatData().paymentMethod[prop]; @@ -15,7 +15,7 @@ const getPropFromPMData = ClientFunction(prop => { const TEST_SPEED = 1; -const iframeSelector = getIframeSelector('.adyen-checkout__payment-method--card iframe'); +const iframeSelector = getIframeSelector('.adyen-checkout__payment-method--scheme iframe'); const cardUtils = cu(iframeSelector); diff --git a/packages/e2e/tests/cards/dualBranding/dualBranding.dropin.test.js b/packages/e2e/tests/cards/dualBranding/dualBranding.dropin.test.js index 135f0e2e9c..1cb915536f 100644 --- a/packages/e2e/tests/cards/dualBranding/dualBranding.dropin.test.js +++ b/packages/e2e/tests/cards/dualBranding/dualBranding.dropin.test.js @@ -4,8 +4,8 @@ import cu from '../utils/cardUtils'; import { DUAL_BRANDED_CARD, REGULAR_TEST_CARD, DUAL_BRANDED_CARD_EXCLUDED } from '../utils/constants'; import { BASE_URL } from '../../pages'; -const dualBrandingIconHolder = Selector('.adyen-checkout__payment-method--card .adyen-checkout__card__dual-branding__buttons'); -const dualBrandingIconHolderActive = Selector('.adyen-checkout__payment-method--card .adyen-checkout__card__dual-branding__buttons--active'); +const dualBrandingIconHolder = Selector('.adyen-checkout__payment-method--scheme .adyen-checkout__card__dual-branding__buttons'); +const dualBrandingIconHolderActive = Selector('.adyen-checkout__payment-method--scheme .adyen-checkout__card__dual-branding__buttons--active'); const NOT_SELECTED_CLASS = 'adyen-checkout__card__cardNumber__brandIcon--not-selected'; @@ -15,7 +15,7 @@ const getPropFromPMData = ClientFunction(prop => { const TEST_SPEED = 1; -const iframeSelector = getIframeSelector('.adyen-checkout__payment-method--card iframe'); +const iframeSelector = getIframeSelector('.adyen-checkout__payment-method--scheme iframe'); const cardUtils = cu(iframeSelector); diff --git a/packages/e2e/tests/cards/kcp/startWithoutKCP/startWithoutKCP.test.js b/packages/e2e/tests/cards/kcp/startWithoutKCP/startWithoutKCP.test.js index 3627f6e733..35a5656045 100644 --- a/packages/e2e/tests/cards/kcp/startWithoutKCP/startWithoutKCP.test.js +++ b/packages/e2e/tests/cards/kcp/startWithoutKCP/startWithoutKCP.test.js @@ -25,7 +25,7 @@ test( 'then complete the form & check component becomes valid', async t => { // For some reason, at full speed, testcafe can fail to fill in the taxNumber correctly - await t.setTestSpeed(0.9); + await t.setTestSpeed(0.5); // Wait for field to appear in DOM await cardPage.numHolder(); @@ -63,7 +63,7 @@ test( 'then delete card number and check taxNumber and password state are cleared', async t => { // For some reason, at full speed, testcafe can fail to fill in the taxNumber correctly - await t.setTestSpeed(0.9); + await t.setTestSpeed(0.5); await cardPage.numHolder(); @@ -89,13 +89,7 @@ test( // Look for expected properties await t.expect(JWETokenArr.length).eql(5); // Expected number of components in the JWE token - await t - .expect(headerObj.alg) - .eql(JWE_ALG) - .expect(headerObj.enc) - .eql(JWE_CONTENT_ALG) - .expect(headerObj.version) - .eql(JWE_VERSION); + await t.expect(headerObj.alg).eql(JWE_ALG).expect(headerObj.enc).eql(JWE_CONTENT_ALG).expect(headerObj.version).eql(JWE_VERSION); // await t.expect(cardPage.getFromState('data.encryptedPassword')).contains('adyenjs_0_1_'); @@ -123,7 +117,7 @@ test( 'then replace card number with non-korean card and expect component to be valid & to be able to pay', async t => { // For some reason, at full speed, testcafe can fail to fill in the taxNumber correctly - await t.setTestSpeed(0.9); + await t.setTestSpeed(0.5); await cardPage.numHolder(); @@ -168,7 +162,7 @@ test( 'expect component not to be valid and for password field to show error', async t => { // For some reason, at full speed, testcafe can fail to fill in the taxNumber correctly - await t.setTestSpeed(0.9); + await t.setTestSpeed(0.5); await cardPage.numHolder(); diff --git a/packages/e2e/tests/cards/threeDS2/threeDS2.default.size.test.js b/packages/e2e/tests/cards/threeDS2/threeDS2.default.size.test.js index 91d27ae968..0a1d0624c8 100644 --- a/packages/e2e/tests/cards/threeDS2/threeDS2.default.size.test.js +++ b/packages/e2e/tests/cards/threeDS2/threeDS2.default.size.test.js @@ -34,7 +34,6 @@ const loggerSubmitThreeDS2 = RequestLogger( fixture`Testing default size of the 3DS2 challenge window, & the challenge flows, on the Card component, since all other tests are for Dropin` .beforeEach(async t => { await t.navigateTo(cardPage.pageUrl); - await turnOffSDKMocking(); }) .clientScripts('./threeDS2.default.size.clientScripts.js') .requestHooks([loggerDetails, loggerSubmitThreeDS2]); @@ -78,9 +77,7 @@ test('#1 Fill in card number that will trigger full flow (fingerprint & challeng .expect(loggerDetails.contains(r => r.response.statusCode === 200)) // Allow time for the /details call, which we expect to be successful .ok({ timeout: 5000 }) - .wait(1000); - - // console.log(logger.requests[1].response.headers); + .wait(3000); // Check the value of the alert text const history = await t.getNativeDialogHistory(); @@ -117,7 +114,7 @@ test('#2 Fill in card number that will trigger challenge-only flow', async t => .expect(loggerDetails.contains(r => r.response.statusCode === 200)) // Allow time for the ONLY details call, which we expect to be successful .ok({ timeout: 5000 }) - .wait(2000); + .wait(3000); // Check the value of the alert text const history = await t.getNativeDialogHistory(); diff --git a/packages/e2e/tests/cards/threeDS2/threeDS2.handleAction.clientScripts.js b/packages/e2e/tests/cards/threeDS2/threeDS2.handleAction.clientScripts.js index 5e1c13c4c5..eb68cff79d 100644 --- a/packages/e2e/tests/cards/threeDS2/threeDS2.handleAction.clientScripts.js +++ b/packages/e2e/tests/cards/threeDS2/threeDS2.handleAction.clientScripts.js @@ -1,5 +1,11 @@ window.dropinConfig = { - showStoredPaymentMethods: false // hide stored PMs so credit card is first on list + showStoredPaymentMethods: false, // hide stored PMs so credit card is first on list + paymentMethodsConfiguration: { + card: { + _disableClickToPay: true, + challengeWindowSize: '04' + } + } }; /** @@ -10,15 +16,5 @@ window.dropinConfig = { * at https://pay.google.com/gp/p/js/pay.js:237:404 */ window.mainConfiguration = { - removePaymentMethods: ['paywithgoogle', 'applepay'], - paymentMethodsConfiguration: { - threeDS2: { - challengeWindowSize: '04' - }, - card: { - _disableClickToPay: true - } - } + removePaymentMethods: ['paywithgoogle', 'applepay'] }; - -window.actionConfigObject = { challengeWindowSize: '01' }; diff --git a/packages/e2e/tests/cards/threeDS2/threeDS2.handleAction.test.js b/packages/e2e/tests/cards/threeDS2/threeDS2.handleAction.test.js index d46accd2f4..962aa00d4b 100644 --- a/packages/e2e/tests/cards/threeDS2/threeDS2.handleAction.test.js +++ b/packages/e2e/tests/cards/threeDS2/threeDS2.handleAction.test.js @@ -11,7 +11,7 @@ import { turnOffSDKMocking } from '../../_common/cardMocks'; const dropinPage = new DropinPage({ components: { - cc: new CardComponentPage('.adyen-checkout__payment-method--card') + cc: new CardComponentPage('.adyen-checkout__payment-method--scheme') } }); @@ -32,7 +32,7 @@ const logger = RequestLogger( const apiVersion = Number(process.env.API_VERSION.substr(1)); -fixture`Testing new (v67) 3DS2 Flow (handleAction config)` +fixture`Testing new (v67) 3DS2 Flow (custom challengeWindowSize config)` .beforeEach(async t => { await t.navigateTo(dropinPage.pageUrl); await turnOffSDKMocking(); @@ -40,7 +40,7 @@ fixture`Testing new (v67) 3DS2 Flow (handleAction config)` .clientScripts('threeDS2.handleAction.clientScripts.js') .requestHooks(logger); -test('#1 Fill in card number that will trigger full flow (fingerprint & challenge) with challenge window size set in the call to handleAction', async t => { +test('#1 Fill in card number that will trigger full flow (fingerprint & challenge) with challenge window size set in the component configuration', async t => { logger.clear(); await dropinPage.cc.numSpan(); @@ -71,7 +71,7 @@ test('#1 Fill in card number that will trigger full flow (fingerprint & challeng // console.log('logger.requests[0].response', logger.requests[0].response); // Check challenge window size is read from config prop set in handleAction call - await t.expect(dropinPage.challengeWindowSize01.exists).ok({ timeout: 3000 }); + await t.expect(dropinPage.challengeWindowSize04.exists).ok({ timeout: 3000 }); // Complete challenge await fillChallengeField(t); @@ -82,7 +82,7 @@ test('#1 Fill in card number that will trigger full flow (fingerprint & challeng .wait(2000) .expect(logger.contains(r => r.request.url.indexOf('/details') > -1 && r.response.statusCode === 200)) .ok() - .wait(1000); + .wait(3000); // Check request body is in the expected form const requestBodyBuffer = logger.requests[1].request.body; @@ -95,7 +95,7 @@ test('#1 Fill in card number that will trigger full flow (fingerprint & challeng await t.expect(history[0].text).eql('Authorised'); }); -test('#2 Fill in card number that will trigger challenge-only flow with challenge window size set in the call to handleAction', async t => { +test('#2 Fill in card number that will trigger challenge-only flow with challenge window size set in component configuration', async t => { logger.clear(); await dropinPage.cc.numSpan(); @@ -114,7 +114,7 @@ test('#2 Fill in card number that will trigger challenge-only flow with challen await t.click(dropinPage.cc.payButton); // Check challenge window size is read from config prop set in handleAction call - await t.expect(dropinPage.challengeWindowSize01.exists).ok({ timeout: 3000 }); + await t.expect(dropinPage.challengeWindowSize04.exists).ok({ timeout: 3000 }); // Complete challenge await fillChallengeField(t); @@ -125,7 +125,7 @@ test('#2 Fill in card number that will trigger challenge-only flow with challen .wait(2000) .expect(logger.contains(r => r.request.url.indexOf('/details') > -1 && r.response.statusCode === 200)) .ok() - .wait(2000); + .wait(3000); // Check the value of the alert text const history = await t.getNativeDialogHistory(); diff --git a/packages/e2e/tests/cards/threeDS2/threeDS2.redirect.test.js b/packages/e2e/tests/cards/threeDS2/threeDS2.redirect.test.js index e41b40e313..131be397c7 100644 --- a/packages/e2e/tests/cards/threeDS2/threeDS2.redirect.test.js +++ b/packages/e2e/tests/cards/threeDS2/threeDS2.redirect.test.js @@ -15,7 +15,7 @@ import CardComponentPage from '../../_models/CardComponent.page'; const dropinPage = new DropinPage({ components: { - cc: new CardComponentPage('.adyen-checkout__payment-method--card') + cc: new CardComponentPage('.adyen-checkout__payment-method--scheme') } }); diff --git a/packages/e2e/tests/cards/threeDS2/threeDS2.test.js b/packages/e2e/tests/cards/threeDS2/threeDS2.test.js index 1798be31b0..5921ce473c 100644 --- a/packages/e2e/tests/cards/threeDS2/threeDS2.test.js +++ b/packages/e2e/tests/cards/threeDS2/threeDS2.test.js @@ -11,7 +11,7 @@ import { turnOffSDKMocking } from '../../_common/cardMocks'; const dropinPage = new DropinPage({ components: { - cc: new CardComponentPage('.adyen-checkout__payment-method--card') + cc: new CardComponentPage('.adyen-checkout__payment-method--scheme') } }); diff --git a/packages/e2e/tests/dropinSessions/dropinSessions.test.js b/packages/e2e/tests/dropinSessions/dropinSessions.test.js index fc5e554974..8d937757fd 100644 --- a/packages/e2e/tests/dropinSessions/dropinSessions.test.js +++ b/packages/e2e/tests/dropinSessions/dropinSessions.test.js @@ -5,7 +5,7 @@ import { getIframeSelector } from '../utils/commonUtils'; import cu from '../cards/utils/cardUtils'; import { TEST_CVC_VALUE } from '../cards/utils/constants'; -const iframeSelector = getIframeSelector('.adyen-checkout__payment-method--card iframe'); +const iframeSelector = getIframeSelector('.adyen-checkout__payment-method--scheme iframe'); const cardUtils = cu(iframeSelector); const { setupLogger, paymentLogger } = loggers; diff --git a/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js b/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js index 9227ff7dd2..8c66638a8d 100644 --- a/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js +++ b/packages/e2e/tests/giftcards/onOrderUpdated/onOrderUpdated.test.js @@ -10,7 +10,7 @@ import { GiftCardSessionPage } from '../../_models/GiftCardComponent.page'; const giftCard = new GiftCardSessionPage(); const { setupLogger, balanceLogger, ordersLogger } = loggers; -const getCallBackData = ClientFunction(() => window.onOrderCreatedTestData); +const getCallBackData = ClientFunction(() => window.onOrderUpdatedTestData); // only setup the loggers for the endpoints so we can setup different responses for different scenarios fixture`Testing gift cards`.page(GIFTCARDS_SESSIONS_URL).requestHooks([setupLogger, balanceLogger, ordersLogger]); diff --git a/packages/e2e/tests/storedCard/general.test.js b/packages/e2e/tests/storedCard/general.test.js index 69e05fae3d..4560e8f50f 100644 --- a/packages/e2e/tests/storedCard/general.test.js +++ b/packages/e2e/tests/storedCard/general.test.js @@ -23,11 +23,11 @@ test('#1 Can fill out the cvc fields in the stored card and make a successful pa await cardPage.cardUtils.fillCVC(t, TEST_CVC_VALUE, 'add', 0); // click pay - await t.click(cardPage.payButton).expect(cardPage.cvcLabelTextError.exists).notOk().wait(1000); + await t.click(cardPage.payButton).expect(cardPage.cvcLabelTextError.exists).notOk().wait(3000); // Check the value of the alert text const history = await t.getNativeDialogHistory(); - await t.expect(history[0].text).eql('Authorised'); + await t.expect(history[0].text).eql('Authorised', { timeout: 5000 }); }); test('#2 Pressing pay without filling the cvc should generate a translated error ("empty")', async t => { @@ -57,9 +57,9 @@ test('#3 A storedCard with no expiry date field still can be used for a successf await cardPage.cardUtils.fillCVC(t, TEST_CVC_VALUE, 'add', 0); // click pay - await t.click(cardPage.payButton).expect(cardPage.cvcLabelTextError.exists).notOk().wait(1000); + await t.click(cardPage.payButton).expect(cardPage.cvcLabelTextError.exists).notOk().wait(3000); // Check the value of the alert text const history = await t.getNativeDialogHistory(); - await t.expect(history[0].text).eql('Authorised'); + await t.expect(history[0].text).eql('Authorised', { timeout: 5000 }); }).clientScripts('./storedCard.noExpiry.clientScripts.js'); // N.B. the clientScript nullifies the expiryMonth & Year fields in the storedCardData diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 3ce2e28767..977238cd30 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -119,6 +119,7 @@ export class DropinComponent extends Component<DropinComponentProps, DropinCompo render(props, { elements, instantPaymentElements, storedPaymentElements, status, activePaymentMethod, cachedPaymentMethods }) { const isLoading = status.type === 'loading'; const isRedirecting = status.type === 'redirect'; + const hasPaymentMethodsToBeDisplayed = elements?.length || instantPaymentElements?.length || storedPaymentElements?.length; switch (status.type) { case 'success': @@ -135,7 +136,7 @@ export class DropinComponent extends Component<DropinComponentProps, DropinCompo <div className={`adyen-checkout__dropin adyen-checkout__dropin--${status.type}`}> {isRedirecting && status.props.component && status.props.component.render()} {isLoading && status.props && status.props.component && status.props.component.render()} - {elements && !!elements.length && ( + {hasPaymentMethodsToBeDisplayed && ( <PaymentMethodList isLoading={isLoading || isRedirecting} isDisablingPaymentMethod={this.state.isDisabling} From ea50de1f3d23b5d602f9dd9c7d2f203d4482284f Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 25 Jan 2024 07:47:33 -0300 Subject: [PATCH 43/55] fixing sonarcloud lint --- .../lib/src/components/ApplePay/ApplePay.tsx | 28 +++---------------- packages/lib/src/components/ApplePay/types.ts | 2 +- .../src/components/GooglePay/GooglePay.tsx | 19 +++---------- .../lib/src/components/GooglePay/utils.ts | 16 ----------- ...at-paypal-order-contact-to-adyen-format.ts | 7 ++--- .../internal/UIElement/UIElement.tsx | 16 +++++++---- packages/lib/src/core/core.ts | 24 ++++------------ packages/lib/src/core/types.ts | 2 -- packages/lib/src/types/global-types.ts | 15 ---------- 9 files changed, 28 insertions(+), 101 deletions(-) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index d949d221a8..b09b4dc701 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -8,12 +8,7 @@ import { httpPost } from '../../core/Services/http'; import { APPLEPAY_SESSION_ENDPOINT } from './config'; import { preparePaymentRequest } from './payment-request'; import { resolveSupportedVersion, mapBrands, formatApplePayContactToAdyenAddressFormat } from './utils'; -import { - ApplePayConfiguration, - ApplePayElementData, - ApplePayPaymentOrderDetails, - ApplePaySessionRequest -} from './types'; +import { ApplePayConfiguration, ApplePayElementData, ApplePayPaymentOrderDetails, ApplePaySessionRequest } from './types'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { TxVariants } from '../tx-variants'; import { PaymentResponseData, RawPaymentResponse } from '../../types/global-types'; @@ -71,13 +66,7 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { }; private startSession() { - const { - version, - onValidateMerchant, - onPaymentMethodSelected, - onShippingMethodSelected, - onShippingContactSelected - } = this.props; + const { version, onValidateMerchant, onPaymentMethodSelected, onShippingMethodSelected, onShippingContactSelected } = this.props; const paymentRequest = preparePaymentRequest({ companyName: this.props.configuration.merchantName, @@ -244,12 +233,7 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { */ public override async isAvailable(): Promise<void> { if (document.location.protocol !== 'https:') { - return Promise.reject( - new AdyenCheckoutError( - 'IMPLEMENTATION_ERROR', - 'Trying to start an Apple Pay session from an insecure document' - ) - ); + return Promise.reject(new AdyenCheckoutError('IMPLEMENTATION_ERROR', 'Trying to start an Apple Pay session from an insecure document')); } if (!this.props.onValidateMerchant && !this.props.clientKey) { @@ -257,11 +241,7 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> { } try { - if ( - window.ApplePaySession && - ApplePaySession.canMakePayments() && - ApplePaySession.supportsVersion(this.props.version) - ) { + if (window.ApplePaySession && ApplePaySession.canMakePayments() && ApplePaySession.supportsVersion(this.props.version)) { return Promise.resolve(); } } catch (error) { diff --git a/packages/lib/src/components/ApplePay/types.ts b/packages/lib/src/components/ApplePay/types.ts index b443b4b704..53a84bd645 100644 --- a/packages/lib/src/components/ApplePay/types.ts +++ b/packages/lib/src/components/ApplePay/types.ts @@ -220,7 +220,7 @@ export interface ApplePayElementData { type: string; applePayToken: string; billingAddress?: AddressData; - shippingAddress?: AddressData; + deliveryAddress?: AddressData; }; } diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 66d94a45c5..342e2f5be2 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -32,14 +32,10 @@ class GooglePay extends UIElement<GooglePayConfiguration> { } formatProps(props): GooglePayConfiguration { - // const allowedCardNetworks = props.brands?.length ? mapBrands(props.brands) : props.allowedCardNetworks; const buttonSizeMode = props.buttonSizeMode ?? (props.isDropin ? 'fill' : 'static'); const buttonLocale = getGooglePayLocale(props.buttonLocale ?? props.i18n?.locale); - const callbackIntents: google.payments.api.CallbackIntent[] = [ - ...props.callbackIntents, - 'PAYMENT_AUTHORIZATION' - ]; + const callbackIntents: google.payments.api.CallbackIntent[] = [...props.callbackIntents, 'PAYMENT_AUTHORIZATION']; return { ...props, @@ -86,16 +82,9 @@ class GooglePay extends UIElement<GooglePayConfiguration> { * * @see https://developers.google.com/pay/api/web/reference/client#onPaymentAuthorized **/ - private onPaymentAuthorized = async ( - paymentData: google.payments.api.PaymentData - ): Promise<google.payments.api.PaymentAuthorizationResult> => { - const billingAddress: AddressData = formatGooglePayContactToAdyenAddressFormat( - paymentData.paymentMethodData.info.billingAddress - ); - const deliveryAddress: AddressData = formatGooglePayContactToAdyenAddressFormat( - paymentData.shippingAddress, - true - ); + private onPaymentAuthorized = async (paymentData: google.payments.api.PaymentData): Promise<google.payments.api.PaymentAuthorizationResult> => { + const billingAddress: AddressData = formatGooglePayContactToAdyenAddressFormat(paymentData.paymentMethodData.info.billingAddress); + const deliveryAddress: AddressData = formatGooglePayContactToAdyenAddressFormat(paymentData.shippingAddress, true); this.setState({ authorizedEvent: paymentData, diff --git a/packages/lib/src/components/GooglePay/utils.ts b/packages/lib/src/components/GooglePay/utils.ts index 8c767d96dd..20e96bd883 100644 --- a/packages/lib/src/components/GooglePay/utils.ts +++ b/packages/lib/src/components/GooglePay/utils.ts @@ -45,22 +45,6 @@ export function formatGooglePayContactToAdyenAddressFormat( }; } -// export function mapBrands(brands) { -// const brandMapping = { -// mc: 'MASTERCARD', -// amex: 'AMEX', -// visa: 'VISA', -// interac: 'INTERAC', -// discover: 'DISCOVER' -// }; -// return brands.reduce((accumulator, item) => { -// if (!!brandMapping[item] && !accumulator.includes(brandMapping[item])) { -// accumulator.push(brandMapping[item]); -// } -// return accumulator; -// }, []); -// } - const supportedLocales = [ 'en', 'ar', diff --git a/packages/lib/src/components/PayPal/utils/format-paypal-order-contact-to-adyen-format.ts b/packages/lib/src/components/PayPal/utils/format-paypal-order-contact-to-adyen-format.ts index 753d3a38d7..747ef64e60 100644 --- a/packages/lib/src/components/PayPal/utils/format-paypal-order-contact-to-adyen-format.ts +++ b/packages/lib/src/components/PayPal/utils/format-paypal-order-contact-to-adyen-format.ts @@ -3,10 +3,7 @@ import { AddressData } from '../../../types/global-types'; /** * This function formats PayPal contact format to Adyen address format */ -export const formatPaypalOrderContatcToAdyenFormat = ( - paymentContact: any, - isDeliveryAddress?: boolean -): AddressData | null => { +export const formatPaypalOrderContatcToAdyenFormat = (paymentContact: any, isDeliveryAddress?: boolean): AddressData | null => { const getStreet = (addressPart1 = null, addressPart2 = null): string | null => { if (addressPart1 && addressPart2) return `${addressPart1}, ${addressPart2}`; if (addressPart1) return addressPart1; @@ -14,7 +11,7 @@ export const formatPaypalOrderContatcToAdyenFormat = ( return null; }; - if (!paymentContact || !paymentContact.address) return null; + if (paymentContact?.address === undefined) return null; const { address, name } = paymentContact; const street = getStreet(address.address_line_1, address.address_line_2); diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index b4b366566e..880d7d9e77 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -9,14 +9,14 @@ import { Resources } from '../../../core/Context/Resources'; import { NewableComponent } from '../../../core/core.registry'; import { ComponentMethodsRef, IUIElement, PayButtonFunctionProps, UIElementProps, UIElementStatus } from './types'; import { + AdditionalDetailsStateData, + CheckoutAdvancedFlowResponse, Order, PaymentAction, PaymentData, PaymentMethodsResponse, - CheckoutAdvancedFlowResponse, PaymentResponseData, - RawPaymentResponse, - AdditionalDetailsStateData + RawPaymentResponse } from '../../../types/global-types'; import './UIElement.scss'; import { CheckoutSessionDetailsResponse, CheckoutSessionPaymentResponse } from '../../../core/CheckoutSession/types'; @@ -249,7 +249,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten private async submitAdditionalDetailsUsingSessionsFlow(data: any): Promise<CheckoutSessionDetailsResponse> { try { - return this.core.session.submitDetails(data); + return await this.core.session.submitDetails(data); } catch (error) { this.handleError(error); return Promise.reject(error); @@ -402,10 +402,16 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten return <PayButton {...props} amount={this.props.amount} secondaryAmount={this.props.secondaryAmount} onClick={this.submit} />; }; + /** + * Used in the Partial Orders flow. + * When the Order is updated, the merchant can request new payment methods based on the new remaining amount + * + * @private + */ private async handleAdvanceFlowPaymentMethodsUpdate(order: Order) { return new Promise<PaymentMethodsResponse>((resolve, reject) => { if (!this.props.onPaymentMethodsRequest) { - return reject(); + return reject(new Error('onPaymentMethodsRequest is not implemented')); } this.props.onPaymentMethodsRequest( diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index bb9795c9c7..bcf61453f8 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -59,18 +59,12 @@ class Core implements ICore { this.setOptions(props); this.loadingContext = resolveEnvironment(this.options.environment, this.options.environmentUrls?.api); - this.cdnContext = resolveCDNEnvironment( - this.options.resourceEnvironment || this.options.environment, - this.options.environmentUrls?.api - ); - this.session = - this.options.session && new Session(this.options.session, this.options.clientKey, this.loadingContext); + this.cdnContext = resolveCDNEnvironment(this.options.resourceEnvironment || this.options.environment, this.options.environmentUrls?.api); + this.session = this.options.session && new Session(this.options.session, this.options.clientKey, this.loadingContext); const clientKeyType = this.options.clientKey?.substr(0, 4); if ((clientKeyType === 'test' || clientKeyType === 'live') && !this.loadingContext.includes(clientKeyType)) { - throw new Error( - `Error: you are using a '${clientKeyType}' clientKey against the '${this.options.environment}' environment` - ); + throw new Error(`Error: you are using a '${clientKeyType}' clientKey against the '${this.options.environment}' environment`); } // Expose version number for npm builds @@ -168,9 +162,7 @@ class Core implements ICore { 'a "resultCode": have you passed in the whole response object by mistake?' ); } - throw new Error( - 'createFromAction::Invalid Action - the passed action object does not have a "type" property' - ); + throw new Error('createFromAction::Invalid Action - the passed action object does not have a "type" property'); } if (action.type) { @@ -260,8 +252,7 @@ class Core implements ICore { * @internal */ private handleCreateError(paymentMethod?): never { - const paymentMethodName = - paymentMethod && paymentMethod.name ? paymentMethod.name : 'The passed payment method'; + const paymentMethodName = paymentMethod?.name ?? 'The passed payment method'; const errorMessage = paymentMethod ? `${paymentMethodName} is not a valid Checkout Component. What was passed as a txVariant was: ${JSON.stringify( paymentMethod @@ -272,10 +263,7 @@ class Core implements ICore { } private createPaymentMethodsList(paymentMethodsResponse?: PaymentMethods): void { - this.paymentMethodsResponse = new PaymentMethods( - this.options.paymentMethodsResponse || paymentMethodsResponse, - this.options - ); + this.paymentMethodsResponse = new PaymentMethods(this.options.paymentMethodsResponse || paymentMethodsResponse, this.options); } private createCoreModules(): void { diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index a16553e454..0ee2643d60 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -191,7 +191,6 @@ export interface CoreConfiguration { actions: { resolve: (response: CheckoutAdvancedFlowResponse) => void; reject: () => void; - // reject: (error?: onSubmitReject) => void; } ): void; @@ -208,7 +207,6 @@ export interface CoreConfiguration { actions: { resolve: (response: CheckoutAdvancedFlowResponse) => void; reject: () => void; - // reject: (error?: onSubmitReject) => void; } ): void; diff --git a/packages/lib/src/types/global-types.ts b/packages/lib/src/types/global-types.ts index 91890daa18..8c3d23b12f 100644 --- a/packages/lib/src/types/global-types.ts +++ b/packages/lib/src/types/global-types.ts @@ -1,6 +1,5 @@ import { ADDRESS_SCHEMA } from '../components/internal/Address/constants'; import actionTypes from '../core/ProcessResponse/PaymentAction/actionTypes'; -// import { onSubmitReject } from '../core/types'; export type PaymentActionsType = keyof typeof actionTypes; @@ -206,19 +205,6 @@ export interface PaymentAmountExtended extends PaymentAmount { currencyDisplay?: string; } -export type ShopperDetails = { - shopperName?: { - firstName?: string; - lastName?: string; - }; - shopperEmail?: string; - countryCode?: string; - telephoneNumber?: string; - dateOfBirth?: string; - billingAddress?: Partial<AddressData>; - shippingAddress?: Partial<AddressData>; -}; - export type AddressField = (typeof ADDRESS_SCHEMA)[number]; export type AddressData = { @@ -336,7 +322,6 @@ export type SessionsResponse = { resultCode: ResultCode; }; -//TODO double check these values export interface PaymentMethodsRequestData { order?: Order; locale?: string; From c49af073002696230fbac85d303454eecf0eb68f Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 25 Jan 2024 09:51:48 -0300 Subject: [PATCH 44/55] attempt to delete types.ts from sonarcloud --- packages/lib/config/jest.config.cjs | 7 ++++++- sonar-project.properties | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/lib/config/jest.config.cjs b/packages/lib/config/jest.config.cjs index 8aafd69dcf..0aeb342961 100644 --- a/packages/lib/config/jest.config.cjs +++ b/packages/lib/config/jest.config.cjs @@ -10,5 +10,10 @@ module.exports = { moduleNameMapper: { '\\.scss$': '<rootDir>/config/testMocks/styleMock.js' }, - coveragePathIgnorePatterns: ['node_modules/', 'config/', 'scripts/'] + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/types.ts', + '!src/language/locales/**' + ], + coveragePathIgnorePatterns: ['node_modules/', 'config/', 'scripts/', 'storybook/', '.storybook/', 'auto/'] }; diff --git a/sonar-project.properties b/sonar-project.properties index f884d83d47..6449952b4c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,3 +4,4 @@ sonar.projectKey=Adyen_adyen-web sonar.sourceEncoding=UTF-8 sonar.javascript.lcov.reportPaths=./packages/lib/coverage/lcov.info +sonar.coverage.exclusions=packages/lib/src/**/types.ts From a2ee22cef015ab67774f0366362251aae9cb82f9 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 25 Jan 2024 10:11:31 -0300 Subject: [PATCH 45/55] moved exclusions to sonarcloud file --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 6449952b4c..c5bedf1ce1 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,4 +4,4 @@ sonar.projectKey=Adyen_adyen-web sonar.sourceEncoding=UTF-8 sonar.javascript.lcov.reportPaths=./packages/lib/coverage/lcov.info -sonar.coverage.exclusions=packages/lib/src/**/types.ts +sonar.coverage.exclusions=packages/lib/src/**/types.ts,packages/lib/src/**/*.test.*,packages/lib/storybook/**/*,packages/e2e/**/*,packages/e2e-playwright/**/*,packages/playground/**/*,packages/lib/config/**/* From d167743e1d9d1c8434b609d798d68fb4322e3982 Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 25 Jan 2024 10:19:10 -0300 Subject: [PATCH 46/55] sonarcloud: adding spec file --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index c5bedf1ce1..8bff498863 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -4,4 +4,4 @@ sonar.projectKey=Adyen_adyen-web sonar.sourceEncoding=UTF-8 sonar.javascript.lcov.reportPaths=./packages/lib/coverage/lcov.info -sonar.coverage.exclusions=packages/lib/src/**/types.ts,packages/lib/src/**/*.test.*,packages/lib/storybook/**/*,packages/e2e/**/*,packages/e2e-playwright/**/*,packages/playground/**/*,packages/lib/config/**/* +sonar.coverage.exclusions=packages/lib/src/**/types.ts,packages/lib/src/**/*.test.*,packages/lib/src/**/*.spec.*,packages/lib/storybook/**/*,packages/e2e/**/*,packages/e2e-playwright/**/*,packages/playground/**/*,packages/lib/config/**/* From 39a5ea888b65e1eae0a716a05efe336d8787358a Mon Sep 17 00:00:00 2001 From: guilhermer <guilherme.ribeiro@adyen.com> Date: Thu, 25 Jan 2024 11:29:24 -0300 Subject: [PATCH 47/55] feat: using error message from i18n --- packages/lib/src/components/GooglePay/GooglePay.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 342e2f5be2..9de377e834 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -110,18 +110,19 @@ class GooglePay extends UIElement<GooglePayConfiguration> { this.setElementStatus('ready'); const googlePayError = paymentResponse.error?.googlePayError; + const fallbackMessage = this.props.i18n.get('error.subtitle.payment'); const error: google.payments.api.PaymentDataError = typeof googlePayError === 'string' || undefined ? { intent: 'PAYMENT_AUTHORIZATION', reason: 'OTHER_ERROR', - message: (googlePayError as string) || 'Payment failed' + message: (googlePayError as string) || fallbackMessage } : { intent: googlePayError?.intent || 'PAYMENT_AUTHORIZATION', reason: googlePayError?.reason || 'OTHER_ERROR', - message: googlePayError?.message || 'Payment failed' + message: googlePayError?.message || fallbackMessage }; resolve({ From b7d9a04af4463a5cd55a16802ce2c6e6540cb98b Mon Sep 17 00:00:00 2001 From: nicholas <nicholas.spong@adyen.com> Date: Wed, 31 Jan 2024 14:35:52 +0100 Subject: [PATCH 48/55] Fixed typescript errors related to mismatching onError function signatures in 3DS2 and Card --- packages/lib/src/components/Card/Card.tsx | 1 - packages/lib/src/components/Card/types.ts | 4 ++-- .../src/components/CustomCard/CustomCard.tsx | 1 - .../components/ThreeDS2/ThreeDS2Challenge.tsx | 7 ++---- .../ThreeDS2/ThreeDS2DeviceFingerprint.tsx | 14 +++-------- .../Challenge/PrepareChallenge3DS2.tsx | 23 +++++++++++++++---- .../PrepareFingerprint3DS2.tsx | 7 +++--- .../lib/src/components/ThreeDS2/config.ts | 3 +++ packages/lib/src/components/ThreeDS2/types.ts | 6 ++--- .../lib/src/core/Errors/AdyenCheckoutError.ts | 17 ++++++++++---- packages/lib/src/core/core.registry.ts | 1 - packages/playground/src/pages/Cards/Cards.js | 3 --- 12 files changed, 47 insertions(+), 40 deletions(-) diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index 787ee03ccb..6538fd4497 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -17,7 +17,6 @@ import UIElement from '../internal/UIElement'; import PayButton from '../internal/PayButton'; import { PayButtonProps } from '../internal/PayButton/PayButton'; -// @ts-ignore TODO: Check with nick export class CardElement extends UIElement<CardConfiguration> { public static type = TxVariants.scheme; diff --git a/packages/lib/src/components/Card/types.ts b/packages/lib/src/components/Card/types.ts index 7dd84e28a3..ba283d3534 100644 --- a/packages/lib/src/components/Card/types.ts +++ b/packages/lib/src/components/Card/types.ts @@ -16,8 +16,8 @@ import { InstallmentOptions } from './components/CardInput/components/types'; import { DisclaimerMsgObject } from '../internal/DisclaimerMessage/DisclaimerMessage'; import { Placeholders } from './components/CardInput/types'; import { UIElementProps } from '../internal/UIElement/types'; +import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; -// @ts-ignore TODO: Check with nick export interface CardConfiguration extends UIElementProps { /** * Only set for a stored card, @@ -150,7 +150,7 @@ export interface CardConfiguration extends UIElementProps { /** * Called in case of an invalid Card Number, invalid Expiry Date, or incomplete field. Called again when errors are cleared. */ - onError?: (event: CbObjOnError) => void; + onError?: (error: CbObjOnError | AdyenCheckoutError) => void; /** * Called when a field gains or loses focus. diff --git a/packages/lib/src/components/CustomCard/CustomCard.tsx b/packages/lib/src/components/CustomCard/CustomCard.tsx index 108a0cf782..69903498eb 100644 --- a/packages/lib/src/components/CustomCard/CustomCard.tsx +++ b/packages/lib/src/components/CustomCard/CustomCard.tsx @@ -15,7 +15,6 @@ import { CustomCardConfiguration } from './types'; // type // countryCode -// @ts-ignore TODO: Check with nick export class CustomCard extends UIElement<CustomCardConfiguration> { public static type = TxVariants.customCard; diff --git a/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx b/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx index e16b956501..1e3003c8da 100644 --- a/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx +++ b/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx @@ -6,8 +6,8 @@ import { existy } from '../internal/SecuredFields/lib/utilities/commonUtils'; import { hasOwnProperty } from '../../utils/hasOwnProperty'; import { TxVariants } from '../tx-variants'; import { ThreeDS2ChallengeConfiguration } from './types'; +import AdyenCheckoutError, { API_ERROR } from '../../core/Errors/AdyenCheckoutError'; -// @ts-ignore TODO: Check with nick class ThreeDS2Challenge extends UIElement<ThreeDS2ChallengeConfiguration> { public static type = TxVariants.threeDS2Challenge; @@ -31,10 +31,7 @@ class ThreeDS2Challenge extends UIElement<ThreeDS2ChallengeConfiguration> { */ const dataTypeForError = hasOwnProperty(this.props, 'isMDFlow') ? 'paymentData' : 'authorisationToken'; - this.props.onError({ - errorCode: 'threeds2.challenge', - message: `No ${dataTypeForError} received. Challenge cannot proceed` - }); + this.props.onError(new AdyenCheckoutError(API_ERROR, `No ${dataTypeForError} received. 3DS2 Challenge cannot proceed`)); return null; } diff --git a/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx b/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx index 4503c2ee64..debf8e0034 100644 --- a/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx +++ b/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx @@ -5,8 +5,8 @@ import callSubmit3DS2Fingerprint from './callSubmit3DS2Fingerprint'; import { existy } from '../internal/SecuredFields/lib/utilities/commonUtils'; import { TxVariants } from '../tx-variants'; import { ThreeDS2DeviceFingerprintConfiguration } from './types'; +import AdyenCheckoutError, { API_ERROR } from '../../core/Errors/AdyenCheckoutError'; -// @ts-ignore TODO: Check with nick class ThreeDS2DeviceFingerprint extends UIElement<ThreeDS2DeviceFingerprintConfiguration> { public static type = TxVariants.threeDS2Fingerprint; @@ -28,10 +28,7 @@ class ThreeDS2DeviceFingerprint extends UIElement<ThreeDS2DeviceFingerprintConfi * In the MDFlow the paymentData is always present (albeit an empty string, which is why we use 'existy' since we should be allowed to proceed with this) */ if (!existy(this.props.paymentData)) { - this.props.onError({ - errorCode: ThreeDS2DeviceFingerprint.defaultProps.dataKey, - message: 'No paymentData received. Fingerprinting cannot proceed' - }); + this.props.onError(new AdyenCheckoutError(API_ERROR, `No paymentData received. 3DS2 Fingerprint cannot proceed`)); return null; } @@ -39,12 +36,7 @@ class ThreeDS2DeviceFingerprint extends UIElement<ThreeDS2DeviceFingerprintConfi * this.props.isMDFlow indicates a threeds2InMDFlow process. It means the action to create this component came from the threeds2InMDFlow process * and upon completion should call the passed onComplete callback (instead of the /submitThreeDS2Fingerprint endpoint for the regular, "native" flow) */ - return ( - <PrepareFingerprint - {...this.props} - onComplete={this.props.isMDFlow ? this.onComplete : this.callSubmit3DS2Fingerprint} - /> - ); + return <PrepareFingerprint {...this.props} onComplete={this.props.isMDFlow ? this.onComplete : this.callSubmit3DS2Fingerprint} />; } } diff --git a/packages/lib/src/components/ThreeDS2/components/Challenge/PrepareChallenge3DS2.tsx b/packages/lib/src/components/ThreeDS2/components/Challenge/PrepareChallenge3DS2.tsx index 43258421bf..cc6195787a 100644 --- a/packages/lib/src/components/ThreeDS2/components/Challenge/PrepareChallenge3DS2.tsx +++ b/packages/lib/src/components/ThreeDS2/components/Challenge/PrepareChallenge3DS2.tsx @@ -1,6 +1,6 @@ import { Component, h } from 'preact'; import DoChallenge3DS2 from './DoChallenge3DS2'; -import { createChallengeResolveData, handleErrorCode, prepareChallengeData, createOldChallengeResolveData } from '../utils'; +import { createChallengeResolveData, prepareChallengeData, createOldChallengeResolveData } from '../utils'; import { PrepareChallenge3DS2Props, PrepareChallenge3DS2State } from './types'; import { ChallengeData, ThreeDS2FlowObject } from '../../types'; import '../../ThreeDS2.scss'; @@ -8,6 +8,8 @@ import Img from '../../../internal/Img'; import './challenge.scss'; import { hasOwnProperty } from '../../../../utils/hasOwnProperty'; import useImage from '../../../../core/Context/useImage'; +import AdyenCheckoutError, { ERROR } from '../../../../core/Errors/AdyenCheckoutError'; +import { THREEDS2_CHALLENGE_ERROR } from '../../config'; class PrepareChallenge3DS2 extends Component<PrepareChallenge3DS2Props, PrepareChallenge3DS2State> { public static defaultProps = { @@ -80,8 +82,15 @@ class PrepareChallenge3DS2 extends Component<PrepareChallenge3DS2Props, PrepareC // Challenge has resulted in an error (no transStatus could be retrieved) - but we still treat this as a valid scenario if (hasOwnProperty(challenge.result, 'errorCode') && challenge.result.errorCode.length) { // Tell the merchant there's been an error - const errorCodeObject = handleErrorCode(challenge.result.errorCode, challenge.result.errorDescription); - this.props.onError(errorCodeObject); + this.props.onError( + new AdyenCheckoutError( + ERROR, + `${THREEDS2_CHALLENGE_ERROR}: ${ + challenge.result.errorDescription ? challenge.result.errorDescription : 'no transStatus could be retrieved' + }`, + { cause: challenge.result.errorCode } + ) + ); } this.setStatusComplete(challenge.result); @@ -91,8 +100,12 @@ class PrepareChallenge3DS2 extends Component<PrepareChallenge3DS2Props, PrepareC * Called when challenge times-out (which is still a valid scenario)... */ if (hasOwnProperty(challenge, 'errorCode')) { - const errorCodeObject = handleErrorCode(challenge.errorCode); - this.props.onError(errorCodeObject); + this.props.onError( + new AdyenCheckoutError(ERROR, `${THREEDS2_CHALLENGE_ERROR}: '3DS2 challenge timed out'`, { + cause: challenge.errorCode + }) + ); + this.setStatusComplete(challenge.result); return; } diff --git a/packages/lib/src/components/ThreeDS2/components/DeviceFingerprint/PrepareFingerprint3DS2.tsx b/packages/lib/src/components/ThreeDS2/components/DeviceFingerprint/PrepareFingerprint3DS2.tsx index 43ff24635a..740765670a 100644 --- a/packages/lib/src/components/ThreeDS2/components/DeviceFingerprint/PrepareFingerprint3DS2.tsx +++ b/packages/lib/src/components/ThreeDS2/components/DeviceFingerprint/PrepareFingerprint3DS2.tsx @@ -3,6 +3,8 @@ import DoFingerprint3DS2 from './DoFingerprint3DS2'; import { createFingerprintResolveData, createOldFingerprintResolveData, handleErrorCode, prepareFingerPrintData } from '../utils'; import { PrepareFingerprint3DS2Props, PrepareFingerprint3DS2State } from './types'; import { FingerPrintData, ResultObject } from '../../types'; +import AdyenCheckoutError, { ERROR } from '../../../../core/Errors/AdyenCheckoutError'; +import { THREEDS2_FINGERPRINT_ERROR } from '../../config'; class PrepareFingerprint3DS2 extends Component<PrepareFingerprint3DS2Props, PrepareFingerprint3DS2State> { public static type = 'scheme'; @@ -23,10 +25,7 @@ class PrepareFingerprint3DS2 extends Component<PrepareFingerprint3DS2Props, Prep this.state = { status: 'error' }; // TODO - confirm that we should do this, or is it possible to proceed to the challenge anyway? // ...in which case we should console.debug the error object and then call: this.setStatusComplete({ threeDSCompInd: 'N' }); - this.props.onError({ - errorCode: this.props.dataKey, - message: 'Missing fingerprintToken parameter' - }); + this.props.onError(new AdyenCheckoutError(ERROR, `${THREEDS2_FINGERPRINT_ERROR}: Missing "token" property from 3DS2Fingerprint action`)); } } diff --git a/packages/lib/src/components/ThreeDS2/config.ts b/packages/lib/src/components/ThreeDS2/config.ts index 1fe676cac6..361756e1eb 100644 --- a/packages/lib/src/components/ThreeDS2/config.ts +++ b/packages/lib/src/components/ThreeDS2/config.ts @@ -1,5 +1,8 @@ import { ThreeDS2FlowObject } from './types'; +export const THREEDS2_FINGERPRINT_ERROR = '3DS2Fingerprint_Error'; +export const THREEDS2_CHALLENGE_ERROR = '3DS2Challenge_Error'; + export const DEFAULT_CHALLENGE_WINDOW_SIZE = '02'; export const THREEDS_METHOD_TIMEOUT = 10000; diff --git a/packages/lib/src/components/ThreeDS2/types.ts b/packages/lib/src/components/ThreeDS2/types.ts index d06279fb04..289f6a36d1 100644 --- a/packages/lib/src/components/ThreeDS2/types.ts +++ b/packages/lib/src/components/ThreeDS2/types.ts @@ -1,15 +1,15 @@ import { ICore } from '../../core/types'; -import { ErrorCodeObject } from './components/utils'; import UIElement from '../internal/UIElement'; import { ActionHandledReturnObject } from '../../types/global-types'; import Language from '../../language'; +import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; export interface ThreeDS2DeviceFingerprintConfiguration { core: ICore; dataKey: string; token: string; notificationURL: string; - onError: (error?: string | ErrorCodeObject) => void; + onError: (error: AdyenCheckoutError, element?: UIElement) => void; paymentData: string; showSpinner: boolean; type: string; @@ -25,7 +25,7 @@ export interface ThreeDS2ChallengeConfiguration { token?: string; dataKey?: string; notificationURL?: string; - onError?: (error: string | ErrorCodeObject) => void; + onError?: (error: AdyenCheckoutError, element?: UIElement) => void; paymentData?: string; size?: string; challengeWindowSize?: '01' | '02' | '03' | '04' | '05'; diff --git a/packages/lib/src/core/Errors/AdyenCheckoutError.ts b/packages/lib/src/core/Errors/AdyenCheckoutError.ts index f3a16862bf..4b84f4a7cf 100644 --- a/packages/lib/src/core/Errors/AdyenCheckoutError.ts +++ b/packages/lib/src/core/Errors/AdyenCheckoutError.ts @@ -2,19 +2,28 @@ interface CheckoutErrorOptions { cause?: any; } +export const NETWORK_ERROR = 'NETWORK_ERROR'; +export const CANCEL = 'CANCEL'; +export const IMPLEMENTATION_ERROR = 'IMPLEMENTATION_ERROR'; +export const API_ERROR = 'API_ERROR'; +export const ERROR = 'ERROR'; + class AdyenCheckoutError extends Error { protected static errorTypes = { /** Network error. */ - NETWORK_ERROR: 'NETWORK_ERROR', + NETWORK_ERROR, /** Shopper canceled the current transaction. */ - CANCEL: 'CANCEL', + CANCEL, /** Implementation error. The method or parameter are incorrect or are not supported. */ - IMPLEMENTATION_ERROR: 'IMPLEMENTATION_ERROR', + IMPLEMENTATION_ERROR, + + /** API error. The API has not returned the expected data */ + API_ERROR, /** Generic error. */ - ERROR: 'ERROR' + ERROR }; public cause: unknown; diff --git a/packages/lib/src/core/core.registry.ts b/packages/lib/src/core/core.registry.ts index 35b421b1bb..49481aa11d 100644 --- a/packages/lib/src/core/core.registry.ts +++ b/packages/lib/src/core/core.registry.ts @@ -23,7 +23,6 @@ const defaultComponents = { }; class Registry implements IRegistry { - // @ts-ignore TODO: Check with nick public componentsMap: Record<string, NewableComponent> = defaultComponents; public supportedTxVariants: Set<string> = new Set(Object.values(TxVariants)); diff --git a/packages/playground/src/pages/Cards/Cards.js b/packages/playground/src/pages/Cards/Cards.js index 40cd4d3504..ab7614bc67 100644 --- a/packages/playground/src/pages/Cards/Cards.js +++ b/packages/playground/src/pages/Cards/Cards.js @@ -85,9 +85,6 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = // holderNameRequired: true, // maskSecurityCode: true, // enableStoreDetails: true - onError: obj => { - console.log('### Cards::onError:: obj=', obj); - }, onBinLookup: obj => { console.log('### Cards::onBinLookup:: obj=', obj); } From 1377445c66ed53f8e1bcb0687cc842e6d37ca5f3 Mon Sep 17 00:00:00 2001 From: nicholas <nicholas.spong@adyen.com> Date: Wed, 31 Jan 2024 15:07:55 +0100 Subject: [PATCH 49/55] Added wait to e2e test to give time for payment to be processed --- packages/e2e/tests/cards/avs/avs.partial.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/e2e/tests/cards/avs/avs.partial.test.js b/packages/e2e/tests/cards/avs/avs.partial.test.js index ea843c7ff7..88acf7b7bf 100644 --- a/packages/e2e/tests/cards/avs/avs.partial.test.js +++ b/packages/e2e/tests/cards/avs/avs.partial.test.js @@ -35,6 +35,8 @@ test('should not validate Postal Code if property data.billingAddress.country is await t.typeText(cardPage.postalCodeInput, INVALID_POSTALCODE); await t.click(cardPage.payButton); + await t.wait(3000); + // Check the value of the alert text const history = await t.getNativeDialogHistory(); await t.expect(history[0].text).eql('Authorised'); From edc36eae086de0a9585e52be28a7da0a0856f59d Mon Sep 17 00:00:00 2001 From: nicholas <nicholas.spong@adyen.com> Date: Thu, 1 Feb 2024 12:08:14 +0100 Subject: [PATCH 50/55] Removed onError type from Card/types --- packages/lib/src/components/Card/types.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/lib/src/components/Card/types.ts b/packages/lib/src/components/Card/types.ts index ba283d3534..e356194dcb 100644 --- a/packages/lib/src/components/Card/types.ts +++ b/packages/lib/src/components/Card/types.ts @@ -3,7 +3,6 @@ import { CbObjOnBinValue, CbObjOnBrand, CbObjOnConfigSuccess, - CbObjOnError, CbObjOnFieldValid, CbObjOnFocus, CbObjOnLoad, @@ -16,7 +15,6 @@ import { InstallmentOptions } from './components/CardInput/components/types'; import { DisclaimerMsgObject } from '../internal/DisclaimerMessage/DisclaimerMessage'; import { Placeholders } from './components/CardInput/types'; import { UIElementProps } from '../internal/UIElement/types'; -import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; export interface CardConfiguration extends UIElementProps { /** @@ -147,11 +145,6 @@ export interface CardConfiguration extends UIElementProps { */ onBrand?: (event: CbObjOnBrand) => void; - /** - * Called in case of an invalid Card Number, invalid Expiry Date, or incomplete field. Called again when errors are cleared. - */ - onError?: (error: CbObjOnError | AdyenCheckoutError) => void; - /** * Called when a field gains or loses focus. */ From 8b88b8f201e78e69cb7f3e773beff8f31b9d4a7a Mon Sep 17 00:00:00 2001 From: nicholas <nicholas.spong@adyen.com> Date: Thu, 1 Feb 2024 12:09:06 +0100 Subject: [PATCH 51/55] Added onValidationError callback for the CustomCard component --- .../Card/components/CardInput/types.ts | 6 +++--- .../CustomCardInput/CustomCardInput.tsx | 19 +++++++++++++++++-- .../lib/src/components/CustomCard/types.ts | 9 ++++++++- .../src/pages/CustomCards/CustomCards.js | 7 +++++-- .../pages/CustomCards/customCards.config.js | 12 ------------ 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/lib/src/components/Card/components/CardInput/types.ts b/packages/lib/src/components/Card/components/CardInput/types.ts index 1f8c8a4577..5b066c5dbb 100644 --- a/packages/lib/src/components/Card/components/CardInput/types.ts +++ b/packages/lib/src/components/Card/components/CardInput/types.ts @@ -168,9 +168,9 @@ export interface FieldError { } export interface SFError { - isValid: boolean; - errorMessage: string; - errorI18n: string; + isValid?: boolean; + errorMessage?: string; + errorI18n?: string; error: string; rootNode: HTMLElement; detectedBrands?: string[]; diff --git a/packages/lib/src/components/CustomCard/CustomCardInput/CustomCardInput.tsx b/packages/lib/src/components/CustomCard/CustomCardInput/CustomCardInput.tsx index a21bc44abd..5243b36624 100644 --- a/packages/lib/src/components/CustomCard/CustomCardInput/CustomCardInput.tsx +++ b/packages/lib/src/components/CustomCard/CustomCardInput/CustomCardInput.tsx @@ -7,7 +7,8 @@ import { BinLookupResponse, CardBrandsConfiguration } from '../../Card/types'; import SFExtensions from '../../internal/SecuredFields/binLookup/extensions'; import { StylesObject } from '../../internal/SecuredFields/lib/types'; import { Resources } from '../../../core/Context/Resources'; -import { Placeholders } from '../../Card/components/CardInput/types'; +import { Placeholders, SFError } from '../../Card/components/CardInput/types'; +import { ValidationError } from '../types'; interface SecuredFieldsProps { autoFocus?: boolean; @@ -113,13 +114,27 @@ function CustomCardInput(props: SecuredFieldsProps) { useEffect(() => { const sfStateErrorsObj = sfp.current.mapErrorsToValidationRuleResult(); + const mappedErrors = { ...errors, ...sfStateErrorsObj }; // maps sfErrors + props.onChange({ data, valid, - errors: { ...errors, ...sfStateErrorsObj }, // maps sfErrors + errors: mappedErrors, isValid: isSfpValid, selectedBrandValue }); + + // Create an array of Validation error objects and send to callback + if (Object.keys(mappedErrors).length) { + const validationErrors: ValidationError[] = Object.entries(mappedErrors).map(([fieldType, error]) => { + const valErr: ValidationError = { + fieldType, + ...(error ? (error as SFError) : { error: '', rootNode: this.props.rootNode }) + }; + return valErr; + }); + this.props.onValidationError(validationErrors); + } }, [data, valid, errors, selectedBrandValue]); /** diff --git a/packages/lib/src/components/CustomCard/types.ts b/packages/lib/src/components/CustomCard/types.ts index 4afa259d82..16765450a7 100644 --- a/packages/lib/src/components/CustomCard/types.ts +++ b/packages/lib/src/components/CustomCard/types.ts @@ -1,4 +1,5 @@ import { CardConfiguration } from '../Card/types'; +import { SFError } from '../Card/components/CardInput/types'; export type CustomCardConfiguration = Omit< CardConfiguration, @@ -20,4 +21,10 @@ export type CustomCardConfiguration = Omit< | 'installmentOptions' | 'showInstallmentAmounts' | 'configuration' ->; +> & { + onValidationError?: (validationErrors: ValidationError[]) => void; +}; + +export type ValidationError = SFError & { + fieldType: string; +}; diff --git a/packages/playground/src/pages/CustomCards/CustomCards.js b/packages/playground/src/pages/CustomCards/CustomCards.js index 62eebccef3..b0a633db2f 100644 --- a/packages/playground/src/pages/CustomCards/CustomCards.js +++ b/packages/playground/src/pages/CustomCards/CustomCards.js @@ -2,7 +2,7 @@ import { AdyenCheckout, CustomCard } from '@adyen/adyen-web'; import '@adyen/adyen-web/styles/adyen.css'; import { makePayment, makeDetailsCall } from '../../services'; -import { styles, setFocus, onBrand, onConfigSuccess, onBinLookup, onChange } from './customCards.config'; +import { styles, setFocus, onBrand, onConfigSuccess, onBinLookup, onChange, setCCErrors } from './customCards.config'; import { styles_si, onConfigSuccess_si, onFieldValid_si, onBrand_si, onError_si, onFocus_si } from './customCards-si.config'; import { fancyStyles, fancyChangeBrand, fancyErrors, fancyFieldValid, fancyFocus } from './customCards-fancy.config'; import { materialStyles, materialFocus, handleMaterialError, onMaterialFieldValid } from './customCards-material.config'; @@ -92,7 +92,10 @@ const initCheckout = async () => { }, onFocus: setFocus, onBinLookup, - onChange + onChange, + onValidationError: errors => { + errors.forEach(setCCErrors); + } // brandsConfiguration: { // synchrony_plcc: { // icon: 'http://localhost:3000/test_images/smartmoney.png' diff --git a/packages/playground/src/pages/CustomCards/customCards.config.js b/packages/playground/src/pages/CustomCards/customCards.config.js index 463ac5e533..5d56999183 100644 --- a/packages/playground/src/pages/CustomCards/customCards.config.js +++ b/packages/playground/src/pages/CustomCards/customCards.config.js @@ -258,18 +258,6 @@ export function onBinLookup(pCallbackObj) { } export function onChange(state, component) { - // From v5 the onError handler is no longer only for card comp related errors - // - so watch state.errors and use it to call the custom card specific 'setErrors' function - if (!!Object.keys(state.errors).length) { - const errors = Object.entries(state.errors).map(([fieldType, error]) => { - return { - fieldType, - ...(error ? error : { error: '', rootNode: component._node }) - }; - }); - errors.forEach(setCCErrors); - } - /** * If we're in a dual branding scenario & the number field becomes valid or is valid and become invalid * - set the brand logos to the required 'state' From cbc28f2363c8b44eaff7d20e2662ff3de331e0ea Mon Sep 17 00:00:00 2001 From: nicholas <nicholas.spong@adyen.com> Date: Mon, 5 Feb 2024 13:19:00 +0100 Subject: [PATCH 52/55] Added optional chaining operator for customCard's onValidationError callback --- .../components/CustomCard/CustomCardInput/CustomCardInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/components/CustomCard/CustomCardInput/CustomCardInput.tsx b/packages/lib/src/components/CustomCard/CustomCardInput/CustomCardInput.tsx index 5243b36624..e0fd097152 100644 --- a/packages/lib/src/components/CustomCard/CustomCardInput/CustomCardInput.tsx +++ b/packages/lib/src/components/CustomCard/CustomCardInput/CustomCardInput.tsx @@ -133,7 +133,7 @@ function CustomCardInput(props: SecuredFieldsProps) { }; return valErr; }); - this.props.onValidationError(validationErrors); + this.props.onValidationError?.(validationErrors); } }, [data, valid, errors, selectedBrandValue]); From 03faf1f2cfc038a3fa2246f44d1c0f49e8ee6c14 Mon Sep 17 00:00:00 2001 From: antoniof <m1aw@users.noreply.github.com> Date: Mon, 29 Jan 2024 19:24:58 +0100 Subject: [PATCH 53/55] fix giftcard callback types --- packages/lib/src/components/Giftcard/types.ts | 26 ++++++++++++++++--- packages/lib/src/core/types.ts | 7 +++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/lib/src/components/Giftcard/types.ts b/packages/lib/src/components/Giftcard/types.ts index f9976e5296..e70e1bfaca 100644 --- a/packages/lib/src/components/Giftcard/types.ts +++ b/packages/lib/src/components/Giftcard/types.ts @@ -1,6 +1,7 @@ import { FunctionComponent } from 'preact'; import { GiftcardFieldsProps } from './components/types'; import { UIElementProps } from '../internal/UIElement/types'; +import { Order, PaymentData } from '../../types/global-types'; export interface GiftCardElementData { paymentMethod: { @@ -11,16 +12,35 @@ export interface GiftCardElementData { }; } +export type balanceCheckResponseType = { + pspReference: string; + resultCode: string; + balance: { + currency: string; + value: number; + }; +}; + +export type onBalanceCheckCallbackType = ( + resolve: (res: balanceCheckResponseType) => void, + reject: (error: Error) => void, + data: GiftCardElementData +) => Promise<void>; + +export type onOrderRequestCallbackType = (resolve: (order: Order) => void, reject: (error: Error) => void, data: PaymentData) => Promise<void>; + // TODO: Fix these types export interface GiftCardConfiguration extends UIElementProps { pinRequired?: boolean; expiryDateRequired?: boolean; brandsConfiguration?: any; brand?: string; - onOrderUpdated?(data): void; - onOrderRequest?(resolve, reject, data): void; - onBalanceCheck?(resolve, reject, data): void; + onOrderUpdated?: (data) => void; + onBalanceCheck?: onBalanceCheckCallbackType; + onOrderRequest?: onOrderRequestCallbackType; + onRequiringConfirmation?(): void; + /** * @internal */ diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 0ee2643d60..6fdb80c4de 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -5,7 +5,6 @@ import { PaymentAction, PaymentMethodsResponse, ActionHandledReturnObject, - PaymentData, CheckoutAdvancedFlowResponse, PaymentMethodsRequestData, SessionsResponse, @@ -16,7 +15,7 @@ import { AnalyticsOptions } from './Analytics/types'; import { RiskModuleOptions } from './RiskModule/RiskModule'; import UIElement from '../components/internal/UIElement/UIElement'; import AdyenCheckoutError from './Errors/AdyenCheckoutError'; -import { GiftCardElementData } from '../components/Giftcard/types'; +import { onBalanceCheckCallbackType, onOrderRequestCallbackType } from '../components/Giftcard/types'; import { SRPanelConfig } from './Errors/types'; import { NewableComponent } from './core.registry'; import Session from './CheckoutSession'; @@ -216,9 +215,9 @@ export interface CoreConfiguration { onError?(error: AdyenCheckoutError, element?: UIElement): void; - onBalanceCheck?(resolve: () => void, reject: () => void, data: GiftCardElementData): Promise<void>; + onBalanceCheck?: onBalanceCheckCallbackType; - onOrderRequest?(resolve: () => void, reject: () => void, data: PaymentData): Promise<void>; + onOrderRequest?: onOrderRequestCallbackType; onPaymentMethodsRequest?( data: PaymentMethodsRequestData, From b5ed5f95c4c2a77a3a8c51c9cdab3d8e95f0b167 Mon Sep 17 00:00:00 2001 From: antoniof <m1aw@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:09:28 +0100 Subject: [PATCH 54/55] AmazonPay storybook --- .../lib/src/components/AmazonPay/types.ts | 16 +-- .../stories/wallets/AmazonPay.stories.tsx | 20 +++ .../stories/wallets/AmazonPayExample.tsx | 132 ++++++++++++++++++ 3 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 packages/lib/storybook/stories/wallets/AmazonPay.stories.tsx create mode 100644 packages/lib/storybook/stories/wallets/AmazonPayExample.tsx diff --git a/packages/lib/src/components/AmazonPay/types.ts b/packages/lib/src/components/AmazonPay/types.ts index 4e912ce462..dab8eac1dc 100644 --- a/packages/lib/src/components/AmazonPay/types.ts +++ b/packages/lib/src/components/AmazonPay/types.ts @@ -1,4 +1,3 @@ -import Language from '../../language/Language'; import { SUPPORTED_LOCALES_EU, SUPPORTED_LOCALES_US } from './config'; import UIElement from '../internal/UIElement/UIElement'; import { UIElementProps } from '../internal/UIElement/types'; @@ -50,7 +49,6 @@ export interface AmazonPayConfiguration extends UIElementProps { currency?: Currency; deliverySpecifications?: DeliverySpecifications; environment?: string; - i18n: Language; loadingContext?: string; locale?: string; merchantMetadata?: MerchantMetadata; @@ -60,14 +58,14 @@ export interface AmazonPayConfiguration extends UIElementProps { productType?: ProductType; recurringMetadata?: RecurringMetadata; returnUrl?: string; - showChangePaymentDetailsButton: boolean; - showOrderButton: boolean; - showPayButton: boolean; - showSignOutButton: boolean; + showChangePaymentDetailsButton?: boolean; + showOrderButton?: boolean; + showPayButton?: boolean; + showSignOutButton?: boolean; signature?: string; - onClick: (resolve, reject) => Promise<void>; - onError: (error, component) => void; - onSignOut: (resolve, reject) => Promise<void>; + onClick?: (resolve, reject) => Promise<void>; + onError?: (error, component) => void; + onSignOut?: (resolve, reject) => Promise<void>; } export interface AmazonPayComponentProps extends AmazonPayConfiguration { diff --git a/packages/lib/storybook/stories/wallets/AmazonPay.stories.tsx b/packages/lib/storybook/stories/wallets/AmazonPay.stories.tsx new file mode 100644 index 0000000000..85f0a714bc --- /dev/null +++ b/packages/lib/storybook/stories/wallets/AmazonPay.stories.tsx @@ -0,0 +1,20 @@ +import { MetaConfiguration, StoryConfiguration } from '../types'; +import { ApplePayConfiguration } from '../../../src/components/ApplePay/types'; +import { AmazonPayConfiguration } from '../../../src/components/AmazonPay/types'; +import { AmazonPayExample } from './AmazonPayExample'; + +type AmazonPayStory = StoryConfiguration<AmazonPayConfiguration>; + +const meta: MetaConfiguration<ApplePayConfiguration> = { + title: 'Wallets/AmazonPay' +}; + +export const Default: AmazonPayStory = { + render: args => { + return <AmazonPayExample contextArgs={args} />; + }, + args: { + countryCode: 'GB' + } +}; +export default meta; diff --git a/packages/lib/storybook/stories/wallets/AmazonPayExample.tsx b/packages/lib/storybook/stories/wallets/AmazonPayExample.tsx new file mode 100644 index 0000000000..945a4eb39d --- /dev/null +++ b/packages/lib/storybook/stories/wallets/AmazonPayExample.tsx @@ -0,0 +1,132 @@ +import { useEffect, useRef, useState } from 'preact/hooks'; +import { createSessionsCheckout } from '../../helpers/create-sessions-checkout'; +import { createAdvancedFlowCheckout } from '../../helpers/create-advanced-checkout'; +import { PaymentMethodStoryProps } from '../types'; +import AmazonPay from '../../../src/components/AmazonPay'; +import { AmazonPayConfiguration } from '../../../src/components/AmazonPay/types'; + +interface AmazonPayExampleProps { + contextArgs: PaymentMethodStoryProps<AmazonPayConfiguration>; +} + +export const AmazonPayExample = ({ contextArgs }: AmazonPayExampleProps) => { + const container = useRef(null); + const checkout = useRef(null); + const [element, setElement] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + + const createCheckout = async () => { + const { useSessions, showPayButton, countryCode, shopperLocale, amount } = contextArgs; + + //URL selection + // http://localhost:3020/iframe.html?id=wallets-amazonpay--default&viewMode=story + // http://localhost:3020/?path=/story/wallets-amazonpay--default + // either has iframe or path + + const urlSearchParams = new URLSearchParams(window.location.search); + const amazonCheckoutSessionId = urlSearchParams.get('amazonCheckoutSessionId'); + const step = urlSearchParams.get('step'); + + // TODO move this to args + const chargeOptions = { + // chargePermissionType: 'Recurring', + // recurringMetadata: { + // frequency: { + // unit: 'Month', + // value: '1' + // } + // } + }; + + checkout.current = useSessions + ? await createSessionsCheckout({ showPayButton, countryCode, shopperLocale, amount }) + : await createAdvancedFlowCheckout({ + showPayButton, + countryCode, + shopperLocale, + amount + }); + + if (step === 'review') { + const amazonPayElement = new AmazonPay({ + core: checkout.current, + ...chargeOptions, + /** + * The merchant will receive the amazonCheckoutSessionId attached in the return URL. + */ + amazonCheckoutSessionId, + cancelUrl: 'http://localhost:3020/iframe.html?id=wallets-amazonpay--default&viewMode=story', + returnUrl: 'http://localhost:3020/iframe.html?id=wallets-amazonpay--default&viewMode=story&step=result' + }); + setElement(amazonPayElement); + } else if (step === 'result') { + const amazonPayElement = new AmazonPay({ + core: checkout.current, + /** + * The merchant will receive the amazonCheckoutSessionId attached in the return URL. + */ + amazonCheckoutSessionId, + showOrderButton: false, + onSubmit: (state, component) => { + return makePayment(state.data) + .then(response => { + if (response.action) { + component.handleAction(response.action); + } else if (response?.resultCode && checkPaymentResult(response.resultCode)) { + alert(response.resultCode); + } else { + // Try handling the decline flow + // This will redirect the shopper to select another payment method + component.handleDeclineFlow(); + } + }) + .catch(error => { + throw Error(error); + }); + }, + onError: e => { + if (e.resultCode) { + alert(e.resultCode); + } else { + console.error(e); + } + } + }); + setElement(amazonPayElement); + } else { + const amazonPayElement = new AmazonPay({ + core: checkout.current, + + productType: 'PayOnly', + ...chargeOptions, + // Regular checkout: + // checkoutMode: 'ProcessOrder' + + // Express Checkout flow: + returnUrl: 'http://localhost:3020/?path=/story/wallets-amazonpay--default&step=review' + }); + setElement(amazonPayElement); + } + }; + + useEffect(() => { + void createCheckout(); + }, [contextArgs]); + + useEffect(() => { + if (element?.isAvailable) { + element + .isAvailable() + .then(() => { + element.mount(container.current); + }) + .catch(error => { + setErrorMessage(error.toString()); + }); + } else if (element) { + element.mount(container.current); + } + }, [element]); + + return <div>{errorMessage ? <div>{errorMessage}</div> : <div ref={container} id="component-root" className="component-wrapper" />}</div>; +}; From 4d1b1cbf75ec4def43837d2b9df18d8a66535932 Mon Sep 17 00:00:00 2001 From: antoniof <m1aw@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:49:06 +0100 Subject: [PATCH 55/55] fix type def amazonpay --- packages/lib/src/components/AmazonPay/AmazonPay.tsx | 6 +++++- packages/lib/src/components/AmazonPay/types.ts | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/components/AmazonPay/AmazonPay.tsx b/packages/lib/src/components/AmazonPay/AmazonPay.tsx index b9398a1671..6c458f8f04 100644 --- a/packages/lib/src/components/AmazonPay/AmazonPay.tsx +++ b/packages/lib/src/components/AmazonPay/AmazonPay.tsx @@ -52,7 +52,7 @@ export class AmazonPayElement extends UIElement<AmazonPayConfiguration> { return getCheckoutDetails(loadingContext, clientKey, request); } - handleDeclineFlow() { + public handleDeclineFlow() { const { amazonCheckoutSessionId, configuration = {}, loadingContext, clientKey } = this.props; if (!amazonCheckoutSessionId) return console.error('Could handle the decline flow. Missing checkoutSessionId.'); @@ -96,6 +96,10 @@ export class AmazonPayElement extends UIElement<AmazonPayConfiguration> { ref={ref => { this.componentRef = ref; }} + showPayButton={this.props.showPayButton} + onClick={this.props.onClick} + onError={this.props.onError} + onSignOut={this.props.onSignOut} {...this.props} /> </CoreProvider> diff --git a/packages/lib/src/components/AmazonPay/types.ts b/packages/lib/src/components/AmazonPay/types.ts index dab8eac1dc..8faf8655d5 100644 --- a/packages/lib/src/components/AmazonPay/types.ts +++ b/packages/lib/src/components/AmazonPay/types.ts @@ -1,7 +1,7 @@ import { SUPPORTED_LOCALES_EU, SUPPORTED_LOCALES_US } from './config'; -import UIElement from '../internal/UIElement/UIElement'; import { UIElementProps } from '../internal/UIElement/types'; import { BrowserInfo, PaymentAmount } from '../../types/global-types'; +import AmazonPayElement from './AmazonPay'; declare global { interface Window { @@ -52,7 +52,7 @@ export interface AmazonPayConfiguration extends UIElementProps { loadingContext?: string; locale?: string; merchantMetadata?: MerchantMetadata; - onSubmit?: (state: any, element: UIElement) => void; + onSubmit?: (state: any, element: AmazonPayElement) => void; payButton?: any; placement?: Placement; productType?: ProductType; @@ -69,6 +69,14 @@ export interface AmazonPayConfiguration extends UIElementProps { } export interface AmazonPayComponentProps extends AmazonPayConfiguration { + showPayButton: boolean; + showSignOutButton?: boolean; + amazonCheckoutSessionId?: string; + showOrderButton?: boolean; + showChangePaymentDetailsButton?: boolean; + onClick: (resolve, reject) => Promise<void>; + onError: (error, component) => void; + onSignOut: (resolve, reject) => Promise<void>; ref: any; }