Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

onAdditionalDetails callback sometimes receives undefined 'actions' parameter #3004

Open
FilipSvozil opened this issue Dec 4, 2024 · 6 comments
Labels
Needs more info Further information is requested

Comments

@FilipSvozil
Copy link

Describe the bug
AdyenCheckout's onAdditionalDetails callback sometimes receive undefined as the third parameter, i.e. we cannot resolve, nor reject.
This occurs approximately 50% of the time (very rough estimate), both in live and test environments.
I noticed it at least with card and google pay.

Is this intended behavior - does it indicate an issue on our side? How should we act when we don't get actions?
If this is unexpected even for you, please let me know what additional information would be helpful, and I will extend our logging to gather more data.

To Reproduce
I didn't find a sure way to reproduce it. It sometimes happens with 3DSChallenge action, sometimes with 3DSFingerprint action.

Expected behavior
According to the types, onAdditionalDetails callback should always get an actions object.
And the documentation also shows that onAdditionalDetails takes 3 parameters, and the code example calls actions.resolve unconditionally.

Desktop (please complete the following information):
According to our logging, it happens at least on following browsers (random selection):

  • Edge 131.0.0.0 Windows
  • Chrome 131.0.0.0 Windows
  • Chrome 109.0.0.0 Windows
  • Chrome 105.0.0.0 OS X
  • Firefox 133.0 Windows
  • Firefox 128.0 Windows
  • Samsung Internet 22.0 Linux
  • Safari 16.3 OS X

Smartphone (please complete the following information):
According to our logging, it happens at least on following browsers (random selection):

  • Android Browser 4.0 Android
  • Chrome Mobile 131.0.0.0 Android
  • Chrome Mobile 123.0.6312.118 Android
  • Safari 17.6.1 iOS

Additional context
We are using npm package: @adyen/[email protected]

import { Card } from '@adyen/adyen-web/auto';
import { AdyenCheckout } from '@adyen/adyen-web/auto';

const checkout = await AdyenCheckout{
    clientKey: ...,
    environment: ... ? 'live' : 'test',
    locale,
    amount: { value, currency },
    countryCode: ...,
    srConfig: { enabled: false },

    onAdditionalDetails(
      state,
      _component,
      actions,
    ): void {
      // actions is undefined here
    },

    // onSubmit, etc...
});

const component = new Card(checkout, ...);

component.mount(...);
@ribeiroguilherme
Copy link
Contributor

Hi @FilipSvozil ,

The actions must always be there as the third parameter. As it can be seen here , the action object is always created before calling the onAdditionalDetails callback.

You mentioned GooglePay component - do you mean when GooglePay triggers the 3DS flow?

What is the tech stack of your app? And which payment methods are you currently offering?

@ribeiroguilherme ribeiroguilherme added the Needs more info Further information is requested label Jan 2, 2025
@FilipSvozil
Copy link
Author

Hi @ribeiroguilherme , thanks for the confirmation.

Yes, GooglePay with 3DS.
Here is one session from live environment from today, according to our logs, abridged:

  • GooglePay's onClick callback was called, we immediately resolved (i.e. we called the first parameter)
  • AdyenCheckout's onSubmit callback was called, we send the /payments request
  • The /payments request is successful, and has an action (with type: 'threeDS2')
  • We pass the action to ICore.createFromAction, and .mount the result
  • no logs for ~30seconds
  • AdyenCheckout's onActionHandled callback was called (we do nothing, only log its parameter)
    • { "componentType": "3DS2Challenge", "actionDescription": "3DS2 challenge iframe loaded" }
  • AdyenCheckout's onAdditionalDetails callback was called. The third parameter is undefined, we ignore it and send the /payments/details request.
  • The /payments/details request is successful, with resultCode "Authorised".

And by callbacks, I mean the ones we pass in as configuration:

  • new GooglePay(iCore, { onClick: xxx, ... })
  • await AdyenCheckout({ onSubmit: xxx, onActionHandled: xxx, onAdditionalDetails: xxx, ... })
  • Where GooglePay and AdyenCheckout are imported from @adyen/adyen-web/auto

Tech stack is react 18, typescript, vite.

We support the following methods (all imported from @adyen/adyen-web/auto):
ApplePay, Bancontact, Blik, Card, ClickToPay, GooglePay, MBWay, Multibanco, Redirect (Ideal), Trustly

@ribeiroguilherme
Copy link
Contributor

Hi @FilipSvozil ,

Your flow seems alright.

Is there any specific reason that you are using the createFromAction in this case?
This method is not really documented and it is used internally by the library.

Can't you use the actions.resolve({ ... }) in this case?

@FilipSvozil
Copy link
Author

Thanks for the pointer and sorry for the delayed response.
I never even thought it might be an internal method 😳. This code has been sitting there for ages, probably since we migrated from drop-in to components. I never questioned it since it worked and was defined in the types. My bad.

We've been using createFromAction to display the 3DS prompt in a different DOM node than the original card/GooglePay component.
We have a workaround in mind that will maintain a stable div reference for the adyen component, while making it appear to be in a different DOM location.

We'll release a version without createFromAction, and I'll post an update once we collect some live data.

@ribeiroguilherme
Copy link
Contributor

@FilipSvozil thanks for the update.

In the meanwhile, do you have a code snippet showcasing your onSubmit and how are you calling createFromAction there ?

@FilipSvozil
Copy link
Author

FilipSvozil commented Jan 20, 2025

@ribeiroguilherme sure, below is the relevant code, with some context.

Regarding our planned solution: We intend to remove both the action: undefined hack and the entire Our3DS component. When an action is received, the OurPaymentMethodList component will render only the relevant payment method (hiding all others). Additionally, we'll modify the styling of OurPaymentMethodList and its children to remove the list-like appearance, while preserving the DOM structure to prevent React reconciliation from removing the node where the Adyen component is .mount()ed.


Current code:

async function onSubmit(submitData, _component, actions) {
  if (!submitData.isValid) {
    return;
  }

  setState(state => ({
    ...state,
    loading: true,
  }));
  const result = await request({
    method: 'POST',
    url: '/payments', // forwarded to Adyen's `/payments` endpoint through our BE
    data: {
      ...submitData.data,
      origin: window.location.origin,
    },
  });

  if (!result.ok) {
    actions?.reject?.();
    setState(state => ({
      ...state,
      loading: false,
      resultCode: getResultCodeFromError(result),
    }));
    return;
  }

  if (result.data.action) {
    setState(state => ({
      ...state,
      loading: false,
      adyenAction: result.data.action, // leads to the `createFromAction` on next render
    }));
  } else {
    setState(state => ({
      ...state,
      loading: false,
      resultCode: getResultCode(result),
    }));
  }

  actions?.resolve?.({
    ...result.data,
    action: undefined, // have to remove action here for `createFromAction` to work
  });
}

Setting adyenAction into our React state causes a different branch to be rendered.
This effectively removes all payment method components' nodes on the next render
(without calling unmount on the Adyen components, the DOM nodes are just removed by React)

if (state.adyenAction) {
  return <Our3DS />;
} else {
  return <OurPaymentMethodList />;
}

Where Our3DS component just renders:

<div ref={divRef} />

And does the following one-time setup (on initial render):

const component = state.checkout.createFromAction(state.adyenAction, {
  challengeWindowSize: '05',
});
component.mount(divRef.current);

Where state.checkout is an instance of ICore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs more info Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants