diff --git a/packages/lib/src/components/Dropin/Dropin.tsx b/packages/lib/src/components/Dropin/Dropin.tsx index 92a0803b33..916e4b3849 100644 --- a/packages/lib/src/components/Dropin/Dropin.tsx +++ b/packages/lib/src/components/Dropin/Dropin.tsx @@ -167,7 +167,6 @@ class DropinElement extends UIElement implements IDropin { const paymentAction: UIElement = this.core.createFromAction(action, { ...props, elementRef: this.elementRef, // maintain elementRef for 3DS2 flow - onAdditionalDetails: this.handleAdditionalDetails, isDropin: true }); diff --git a/packages/lib/src/components/ThreeDS2/components/utils.ts b/packages/lib/src/components/ThreeDS2/components/utils.ts index f4616cc083..510a78ed21 100644 --- a/packages/lib/src/components/ThreeDS2/components/utils.ts +++ b/packages/lib/src/components/ThreeDS2/components/utils.ts @@ -206,6 +206,7 @@ export const get3DS2FlowProps = (actionSubtype, props) => { // Challenge return { statusType: 'custom', - i18n: props.i18n + i18n: props.i18n, + elementRef: props.elementRef }; }; diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 5a22f62db4..a601e62069 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -372,8 +372,7 @@ export abstract class UIElement

exten const paymentAction = this.core.createFromAction(action, { ...this.elementRef.props, - ...props, - onAdditionalDetails: this.handleAdditionalDetails + ...props }); if (paymentAction) { diff --git a/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts b/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts index ddb9699266..560adbf22d 100644 --- a/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts +++ b/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts @@ -20,7 +20,7 @@ const getActionHandler = statusType => { const config = { ...props, ...action, - onComplete: props.onAdditionalDetails, + onComplete: props.onComplete, onError: props.onError, statusType, originalAction: action @@ -57,7 +57,7 @@ const actionTypes = { token: action.token, paymentData, onActionHandled: props.onActionHandled, - onComplete: props.isMDFlow ? props.onComplete : props.onAdditionalDetails, + onComplete: props.onComplete, onError: props.onError, isDropin: !!props.isDropin, loadingContext: props.loadingContext, diff --git a/packages/lib/src/core/core.test.ts b/packages/lib/src/core/core.test.ts index 979fb73394..23825fab9e 100644 --- a/packages/lib/src/core/core.test.ts +++ b/packages/lib/src/core/core.test.ts @@ -163,6 +163,70 @@ describe('Core', () => { // @ts-ignore showSpinner should be undefined for threeDS2Challenge expect(actionComponent.props.showSpinner).not.toBeDefined(); }); + + test('should call onAdditionalDetails with correct params when the action object calls onComplete', async () => { + const onAdditionalDetails = jest.fn().mockName('onAdditionalDetailsGlobal'); + const checkout = new AdyenCheckout({ + countryCode: 'US', + environment: 'test', + clientKey: 'test_123456', + onAdditionalDetails + }); + await checkout.initialize(); + + AdyenCheckout.register(BCMCMobileElement); + const paymentAction = checkout.createFromAction({ + paymentMethodType: 'bcmc_mobile_QR', + qrCodeData: 'BEP://1bcmc-test.adyen.com/pal/bep$ZTHYT3DHKVXYJ3GHBQNNCX4M', + type: 'qrCode', + paymentData: 'test' + }); + + // @ts-ignore onComplete is not public method, although we call it here to test the callback + paymentAction.onComplete({}); + expect(onAdditionalDetails).toHaveBeenCalledWith( + {}, + expect.any(BCMCMobileElement), + expect.objectContaining({ + resolve: expect.any(Function), + reject: expect.any(Function) + }) + ); + }); + + test('should call submitDetails with correct params when the action object calls onComplete and no component is defined', async () => { + const onAdditionalDetails = jest.fn().mockName('onAdditionalDetailsGlobal'); + const checkout = new AdyenCheckout({ + countryCode: 'US', + environment: 'test', + clientKey: 'test_123456', + onAdditionalDetails + }); + await checkout.initialize(); + + // Overwrite core.submitDetails + const submitDetails = jest.fn(() => {}); + checkout.submitDetails = submitDetails; + + AdyenCheckout.register(BCMCMobileElement); + const paymentAction = checkout.createFromAction({ + paymentMethodType: 'bcmc_mobile_QR', + qrCodeData: 'BEP://1bcmc-test.adyen.com/pal/bep$ZTHYT3DHKVXYJ3GHBQNNCX4M', + type: 'qrCode', + paymentData: 'test' + }); + + // @ts-ignore onComplete is not public method - we need to overwrite UIElement.onComplete to test this edge-case scenario + paymentAction.onComplete = state => { + if (paymentAction.props.onComplete) paymentAction.props.onComplete(state, null); + }; + + // @ts-ignore onComplete is not public method, although we call it here to test the callback + paymentAction.onComplete({ data: { foo: 'bar' } }); + + // should be called with state.data + expect(submitDetails).toHaveBeenCalledWith({ foo: 'bar' }); + }); }); describe('Props order', () => { @@ -264,7 +328,15 @@ describe('Core', () => { // @ts-ignore onComplete is not public method, although we call it here to test the callback paymentAction.onComplete({}); - expect(onAdditionalDetailsCreateFromAction).toHaveBeenCalledWith({}, expect.any(BCMCMobileElement)); + + expect(onAdditionalDetailsCreateFromAction).toHaveBeenCalledWith( + {}, + expect.any(BCMCMobileElement), + expect.objectContaining({ + resolve: expect.any(Function), + reject: expect.any(Function) + }) + ); }); }); diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 285374d6a1..f2cf1c8286 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -269,7 +269,16 @@ class Core implements ICore { const props = { ...this.getCorePropsForComponent(), - ...options + onComplete: (state: AdditionalDetailsData, component?: UIElement) => { + if (component) { + // We use a type assertion to call the protected 'handleAdditionalDetails' method from the UIElement. + // This is safe because this is internal framework code. + (component as unknown as { handleAdditionalDetails: (state: AdditionalDetailsData) => void }).handleAdditionalDetails(state); + } else { + this.submitDetails(state.data); // Fallback. Not sure if there are circumstances in which this will ever fire? But we have a unit test for it, just in case. + } + }, + ...options // allow for any passed options to overwrite the mapped onComplete fn, above e.g. in the MDFlow we want to use the original, passed, onComplete fn }; return getComponentForAction(this, registry, action, props); diff --git a/packages/playground/src/pages/ThreeDS/ThreeDS.html b/packages/playground/src/pages/ThreeDS/ThreeDS.html index ab97c48fb6..a6d84647fd 100644 --- a/packages/playground/src/pages/ThreeDS/ThreeDS.html +++ b/packages/playground/src/pages/ThreeDS/ThreeDS.html @@ -13,13 +13,59 @@

+

ThreeDS2

+ + + + +
-
+
+
+
+
+
+

3DS2:

+ + + + + + + + + + + + + + + + + + + + + + +
+ 3DS2 test cards +
3DS2 ActionCard Number
Frictionless flow (mc)5201281505129736
Challenge-only flow (visa)4212345678910006
Full flow (maestro)5000550000000029
+

+ + More information → + +

+