Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a753484
Fixing issue with actions in onAdditionalDetails, in the core
sponglord Jul 25, 2025
e55b15f
Apply fix to 3DS2
sponglord Jul 25, 2025
3299976
Playground ThreeDS component made more user-friendly - when it comes …
sponglord Aug 14, 2025
3b3fa4f
Added comments about handling MDFlow
sponglord Aug 14, 2025
cf9f561
Playground ThreeDS component has checkbox to choose createFromAction …
sponglord Aug 15, 2025
1ec9ee2
Testing MDFlow
sponglord Aug 15, 2025
48e6d32
Merge branch 'main' into fix/onAdditionalDetails_createFromAction_pro…
sponglord Aug 15, 2025
654c2ce
Testing MDFlow - works!
sponglord Aug 15, 2025
8bcb528
Changed mocked data for MDFlow
sponglord Aug 15, 2025
ebd7d6a
Fix for tests that were breaking due to Dropin ref being lost
sponglord Aug 18, 2025
8a6e278
Added optional chaining operator for unit tests
sponglord Aug 18, 2025
2ca6de2
Fixing unit test
sponglord Aug 18, 2025
76ececa
props.originalOptions not needed for Dropin/storedCard scenario
sponglord Aug 18, 2025
becaa28
Stop Dropin adding an onAdditionalDetails prop in its own handleActio…
sponglord Aug 19, 2025
f765ee6
Dropped special mapping function for Dropin/handleAction/Challenge an…
sponglord Aug 21, 2025
ae06913
Removed unused code
sponglord Aug 21, 2025
51d0841
Added unit test for edge case
sponglord Aug 21, 2025
5c8adb2
Removed comment
sponglord Aug 21, 2025
de7f3ed
Changed where we write the mapped onComplete function, to allow for i…
sponglord Aug 21, 2025
024abe9
Merge branch 'main' into fix/onAdditionalDetails_createFromAction_pro…
sponglord Sep 10, 2025
a6725df
Adjusted comment
sponglord Sep 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/lib/src/components/Dropin/Dropin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ class DropinElement extends UIElement<DropinConfiguration> implements IDropin {
const paymentAction: UIElement = this.core.createFromAction(action, {
...props,
elementRef: this.elementRef, // maintain elementRef for 3DS2 flow
onAdditionalDetails: this.handleAdditionalDetails,
isDropin: true
});

Expand Down
3 changes: 2 additions & 1 deletion packages/lib/src/components/ThreeDS2/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export const get3DS2FlowProps = (actionSubtype, props) => {
// Challenge
return {
statusType: 'custom',
i18n: props.i18n
i18n: props.i18n,
elementRef: props.elementRef
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,7 @@ export abstract class UIElement<P extends UIElementProps = UIElementProps> exten

const paymentAction = this.core.createFromAction(action, {
...this.elementRef.props,
...props,
onAdditionalDetails: this.handleAdditionalDetails
...props
});

if (paymentAction) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const getActionHandler = statusType => {
const config = {
...props,
...action,
onComplete: props.onAdditionalDetails,
onComplete: props.onComplete,
onError: props.onError,
statusType,
originalAction: action
Expand Down Expand Up @@ -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,
Expand Down
74 changes: 73 additions & 1 deletion packages/lib/src/core/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
})
);
});
});

Expand Down
11 changes: 10 additions & 1 deletion packages/lib/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
48 changes: 47 additions & 1 deletion packages/playground/src/pages/ThreeDS/ThreeDS.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,59 @@
<form class="merchant-checkout__form" method="post">
<div class="merchant-checkout__payment-method">
<div class="merchant-checkout__payment-method__header">
<div style="display:flex;justify-content:space-between;">
<h2>ThreeDS2</h2>
<span style="margin-top:-3px;font-size:0.9em">
<label for="useCreateFromAction">Handle 3DS2 using <i>createFromAction</i></label>
<input type="checkbox" id="useCreateFromAction" name="useCreateFromAction" value="compat">
</span>
</div>
</div>
<div class="merchant-checkout__payment-method__details">
<div class="threeds-field"></div>
<div class="card-field"></div>
</div>
</div>
</form>
<div class="merchant-checkout__form">
<div class="threeds-field"></div>
</div>
<div class="info">
<h2>3DS2:</h2>
<table style="margin-bottom: 0">
<caption>
3DS2 test cards
</caption>
<thead>
<tr>
<th scope="col">3DS2 Action</th>
<th scope="col">Card Number</th>
</tr>
</thead>
<tbody>
<tr>
<td>Frictionless flow (mc)</td>
<td>5201281505129736</td>
</tr>
<tr>
<td>Challenge-only flow (visa)</td>
<td>4212345678910006</td>
</tr>
<tr>
<td>Full flow (maestro)</td>
<td>5000550000000029</td>
</tr>
</tbody>
</table>
<p>
<a
href="https://docs.adyen.com/development-resources/testing/3d-secure-2-authentication/"
target="_blank"
rel="noopener noreferrer"
>
More information →
</a>
</p>
</div>
</main>

<script type="text/javascript">
Expand Down
Loading
Loading