Vue Storefront is now Alokai! Learn More
Legacy Adyen commercetools to SDK v2

Legacy Adyen commercetools to SDK v2

We created this migration guide to help you get up and running quickly with the new Adyen integration. If you have any questions, please reach out to us on Discord, or contact your dedicated Alokai Customer Support Contact.

There were breaking changes in v11. If you do not upgrade your extension and notification module to version ^11 payments will not work.

The extension and notification module versions ^11 are not compatible with @vsf-enterprise/adyen-commercetools You must manage the deployment so that compatible versions are deployed together.

See the compatibility matrix to see which versions are compatible with one another.

Prerequisites

Before you start, make sure you have a project that meets the following requirements:

1

Upgrade packages

Install new packages

yarn
yarn add @vue-storefront/sdk @vsf-enterprise/adyen-commercetools-sdk @vsf-enterprise/adyen-commercetools-api

Remove old packages

yarn
yarn remove @vsf-enterprise/adyen-commercetools

2

Update configuration

Remove payment error view

nuxt.config.js
- { 
-   name: 'adyen-payment-error',
-   path: '/adyen-payment-error',
-   component: resolve(__dirname, 'pages/AdyenPaymentError.vue')
- }

Update useRawSource configuration

nuxt.config.js
{
  useRawSource: {
-   dev: ['@vue-storefront/core', '@vsf-enterprise/commercetools', '@vsf-enterprise/adyen-commercetools'],
-   prod: ['@vue-storefront/core', '@vsf-enterprise/commercetools', '@vsf-enterprise/adyen-commercetools'],
+   dev: ['@vue-storefront/core', '@vsf-enterprise/commercetools', '@vsf-enterprise/adyen-commercetools-sdk'],
+   prod: ['@vue-storefront/core', '@vsf-enterprise/commercetools', '@vsf-enterprise/adyen-commercetools-sdk']
  }
}

Remove outdated Adyen module configuration

nuxt.config.js
- ['@vsf-enterprise/adyen-commercetools/nuxt', {
-   availablePaymentMethods: ['scheme', 'paypal', 'ideal', 'klarna', 'klarna_account', 'klarna_paynow', 'paywithgoogle'],
-   clientKey: '****',
-   environment: 'test',
-   recurringPayments: true,
-   methods: {
-     paypal: {
-       merchantId: '****',
-       intent: 'authorize'
-     }
-   }
- }],

Update Adyen commercetools integration's configuration in middleware.config.js

middleware.config.js
adyen: {
- location: '@vsf-enterprise/adyen-commercetools/server',
+ location: '@vsf-enterprise/adyen-commercetools-api/server',
  configuration: {
    ctApi: {
      apiHost: '<API_HOST>',
      authHost: '<AUTH_HOST>',
      projectKey: '<PROJECT_KEY>
      clientId: '<CLIENT_ID',
      clientSecret: '<CLIENT_SECRET>',
-     scopes: ['manage_project:<CT_PROJECT_KEY>']
+     scopes: ['manage_payments:<CT_PROJECT_KEY>', 'manage_orders:<CT_PROJECT_KEY>', 'view_types:<CT_PROJECT_KEY>']
    },
    adyenApiKey: '<ADYEN_API_KEY>',
    adyenMerchantAccount: '<ADYEN_MERCHANT_ACCOUNT>',
-   adyenRecurringApiBaseUrl: 'https://pal-test.adyen.com',
-   adyenCheckoutApiBaseUrl: '<ADYEN_CHECKOUT_API_BASE_URL>',
+   adyenEnvironment: 'TEST',
+   integrationName: 'adyen',
+   origin: 'https://my-alokai-frontend.local', // URL of frontend
-   buildRedirectUrlAfterAuth: (paymentAndOrder) => `/checkout/thank-you?paymentId=${paymentAndOrder.id}`,
-   buildRedirectUrlAfterError: () => '/adyen-payment-error'
-   buildCustomPaymentAttributes: (client: CommercetoolsClient & { cartId: string, paymentId: string }): Promise<Record<string, any>> => {},
+   buildCustomPaymentAttributes: (params: BuildCustomPaymentAttributesParams): Promise<Record<string, any>> => {},
  }
}

The signature of the buildCustomPaymentAttributes function changed. But it's still used to extend payload sent to the POST /payments.

If your Adyen instance is running in production and taking payments, then you also need to pass buildCustomAdyenClientParameters. It's a function for overwriting object used to instantiate Client from @adyen/api-library. Mostly all you need to is pass what you get and append liveEndpointUrlPrefix as such:

adyen: {
  location: '@vsf-enterprise/adyen-commercetools-api/server',
  configuration: {
    // ...
    buildCustomAdyenClientParameters(params) {
      return {
        ...params,
        liveEndpointUrlPrefix: "YOUR_LIVE_URL_PREFIX"
      }
    }
  }
},

Coming back from redirect based payment method

When user comes back from redirect based payment method they land on our /redirectBack endpoint. There we submits additional information related to performed payment and redirects where onRedirectBack function told us to.

This is the body of default onRedirectBack implementation:

// storefront-middleware/integrations/adyen/config.ts
import type { TOnRedirectBack } from "@vsf-enterprise/adyen-commercetools-api";

const onRedirectBack: TOnRedirectBack = async ({ config }, { ct }, cart) => {
  const order = await ct.orders.create(cart);
  const url = new URL(
    `/order/success?orderId=${order.id}&redirect_status=succeeded`,
    config.origin,
  );
  return url.toString();
};

export const config = {
  location: "@vsf-enterprise/adyen-commercetools-api/server",
  configuration: {
    // ...
    onRedirectBack,
  },
};

Feel free to adjust it and register own implementation using onRedirectBack property in storefront-middleware/integrations/adyen/config.ts.

3

Delete pages/AdyenPaymentError.vue

Delete AdyenPaymentError.vue from pages directory

4

Initialize SDK

Create a directory and file for the SDK configuration (@/sdk/index.ts):

It's fine to still use composable to communicate with commercetools. But not for Adyen anymore. We installed both here for simplicity.

sdk/index.ts
import { initSDK, buildModule } from '@vue-storefront/sdk';
import { adyenCtModule } from '@vsf-enterprise/adyen-commercetools-sdk';

export const sdk = initSDK({
  adyen: buildModule(
    adyenCtModule, {
      apiUrl: 'http://localhost/api/adyen',
      adyenClientKey: '<ADYEN_CLIENT_KEY>',
      adyenEnvironment: '<ADYEN_ENV>'
    }
  )
});

5

Update pages/Checkout/Payment.vue

Replace PaymentAdyenProvider in template:

pages/Checkout/Payment.vue
<template>
  ...
  <PaymentAdyenProvider
      :afterPay="afterPayAndOrder"
  />
  ...
</template>

With:

pages/Checkout/Payment.vue
<template>
  ...
  <div id="adyen-payment-element"></div>
  ...
</template>

Import useRoute:

pages/Checkout/Payment.vue
- import { ref, computed, watch, onMounted, useRouter, useContext } from '@nuxtjs/composition-api';
+ import { ref, computed, watch, onMounted, useRoute, useRouter, useContext } from '@nuxtjs/composition-api';

Remove PaymentAdyenProvider import:

pages/Checkout/Payment.vue
- import PaymentAdyenProvider from '@vsf-enterprise/adyen-commercetools/src/PaymentAdyenProvider';

Import sdk:

pages/Checkout/Payment.vue
<script>
import { sdk } from '~/sdk';
</script>

Add useRoute to setup:

pages/Checkout/Payment.vue
<script>
export default {
  setup() {
    const route = useRoute();
    ...
  }
}
</script>

Replace afterPayAndOrder with createAdyenDropin in setup:

Remove afterPayAndOrder function and return statement:

pages/Checkout/Payment.vue
<script>
export default {
  setup() {
    ...
-    const afterPayAndOrder = async ({ order }) => {
-      context.root.$router.push(`/checkout/thank-you?order=${order.id}`);
-      setCart(null);
-    };

    return {
-      afterPayAndOrder,
    ...
  }
}
</script>

Fetch available payment methods and mount payment element:

You don't need to export the sdk variable for the template

function mapPaymentMethodsResponse(getPaymentMethodsResponse: GetPaymentMethodsResponse) {
  return {
    paymentMethods: getPaymentMethodsResponse.paymentMethods,
    ...(getPaymentMethodsResponse.storedPaymentMethods
      ? {
          storedPaymentMethods: getPaymentMethodsResponse.storedPaymentMethods,
        }
      : {}),
  };
}

async function createAdyenDropin({ shopperLocale }) {
  let getPaymentMethodsResponse = await sdk.adyen.getPaymentMethods({ shopperLocale });
  let balanceCheckResponse: BalanceCheckResponse | null;
  async function handleResult(
    checkout: { update: Function },
    result: MakePaymentResponse | SubmitDetailsResponse,
    dropin: { unmount: Function; handleAction: Function },
  ) {
    if (['Refused', 'Cancelled', 'Error'].includes(result.resultCode!)) {
      // Handling negative result codes and unmounting the Adyen Drop-in
      // To allow the user to try again by recreating session and component
      dropin.unmount();
      // Show some meaningful error message
      return await createAdyenDropin();
    } else if ('action' in result && result.action) {
      // @ts-ignore: next-line
      return dropin.handleAction(result.action);
    } else if (result.order && result.order?.remainingAmount?.value && result.order?.remainingAmount?.value > 0) {
      getPaymentMethodsResponse = await sdk.adyen.getPaymentMethods({ shopperLocale });

      checkout.update({
        paymentMethodsResponse: mapPaymentMethodsResponse(getPaymentMethodsResponse),
        order: getPaymentMethodsResponse.order,
        amount: result.order.remainingAmount,
      });
      return result;
    } else {
      // Here put a code to place an order and redirect to success page.
      console.log({...data})
      return result;
    }
  }

  function createOnOrderCancel(checkout: { update: Function }) {
    return async function onOrderCancel(order: CancelOrderParams) {
      await sdk.adyen.cancelOrder(order);
      getPaymentMethodsResponse = await sdk.adyen.getPaymentMethods({ shopperLocale });
      checkout.update({
        paymentMethodsResponse: mapPaymentMethodsResponse(getPaymentMethodsResponse),
        amount: {
          value: getPaymentMethodsResponse.payment.amountPlanned.centAmount,
          currency: getPaymentMethodsResponse.payment.amountPlanned.currencyCode,
        },
        order: undefined,
      });
    };
  }

  const { checkout } = await sdk.adyen.mountPaymentElement({
    getPaymentMethodsResponse,
    locale: 'en-US',
    order: getPaymentMethodsResponse.order,
    paymentDOMElement: '#adyen-payment-element',
    adyenConfiguration: {
      paymentMethodsConfiguration: {
        card: {
          enableStoreDetails: true
        },
        giftcard: {
          async onBalanceCheck(resolve, reject, data) {
            balanceCheckResponse = await sdk.adyen.balanceCheck({
              ...data,
              paymentId: getPaymentMethodsResponse.payment.id,
            });
            resolve(balanceCheckResponse);
          },
          async onOrderRequest(resolve, reject, data) {
            sdk.adyen
              .createOrder({
                ...data,
                paymentId: getPaymentMethodsResponse.payment.id,
              })
              .then(resolve)
              .catch(reject);
          },
          async onOrderCancel(order) {
            const onOrderCancel = createOnOrderCancel(checkout);
            await onOrderCancel(order);
          },
        },
      },
      async onSubmit(state, dropin) {
        const result = await sdk.adyen.makePayment({
          paymentId: getPaymentMethodsResponse.payment.id,
          componentData: state.data,
          ...(balanceCheckResponse?.resultCode === 'NotEnoughBalance'
            ? {
                balance: balanceCheckResponse.balance,
              }
            : {}),
        });
        balanceCheckResponse = null;
        await handleResult(checkout, result, dropin);
      },
      async onAdditionalDetails(state, dropin) {
        const result = await sdk.adyen.submitDetails({
          paymentId: getPaymentMethodsResponse.payment.id,
          componentData: state.data,
        });
        await handleResult(checkout, result, dropin);
      },
    },
    dropinConfiguration: {
      showRemovePaymentMethodButton: true,
      async onOrderCancel(order) {
        const onOrderCancel = createOnOrderCancel(checkout);
        await onOrderCancel(order);
      },
      async onDisableStoredPaymentMethod(recurringDetailReference, resolve, reject) {
        sdk.adyen.removeCard({ recurringDetailReference }).then(resolve).catch(reject);
      },
    },
  });
}

// Call it client-side, e.g. in Nuxt it would be inside onMounted
const shopperLocale = 'en-US'; // https://docs.adyen.com/api-explorer/Checkout/71/post/paymentMethods#request-shopperLocale
await createAdyenDropin({ shopperLocale });

Call createAdyenDropin in onMounted.

6

Update the extension and notification module to version ^11.5.1.

You have to update the extension and notification module to version ^11.5.1.

See:

Make sure you also adjusted commercetools custom types to match new shapes from the newest versions of extension and notification module.

7

Extend commercetools cart/order type

In commercetools, a cart and an order share a single type because at some point cart is transformed to the order. The name of the shared type is order. We are going to extend this shared type because...

Adyen creates a wrapper for payment partials. To make it accessible after refresh of website or outside of user's session, we had to store it somewhere. Our pick is an extended commercetools' order type.

Fortunately, you do not need to create every cart using this type, we will convert type of the cart when it's necessary (making partial payment).

In order to register extended order type:

  1. Open API Playground,
  2. Make sure you've selected your desired project in the select input placed inside the page's header,
  3. Set:
  • Endpoint as Types,
  • Command as Create,
  • Payload as:
{
  "key": "ctcart-adyen-integration",
  "name": {
    "en": "commercetools Adyen integration cart custom type to support partial payments"
  },
  "resourceTypeIds": ["order"],
  "fieldDefinitions": [
    {
      "name": "adyenOrderData",
      "label": {
        "en": "adyenOrderData"
      },
      "type": {
        "name": "String"
      },
      "inputHint": "SingleLine",
      "required": false
    },
    {
      "name": "adyenOrderPspReference",
      "label": {
        "en": "adyenOrderPspReference"
      },
      "type": {
        "name": "String"
      },
      "inputHint": "SingleLine",
      "required": false
    },
    {
      "name": "adyenOrderRemainingAmountValue",
      "label": {
        "en": "adyenOrderRemainingAmountValue"
      },
      "type": {
        "name": "Number"
      },
      "inputHint": "SingleLine",
      "required": false
    },
    {
      "name": "adyenOrderRemainingAmountCurrency",
      "label": {
        "en": "adyenOrderRemainingAmountCurrency"
      },
      "type": {
        "name": "String"
      },
      "inputHint": "SingleLine",
      "required": false
    }
  ]
}
  1. Click Go!!! button.

Did you already extended commercetools' order type?

It is possible that your application is already extending order type and you cannot stop using it. In that case, we recommend adding fields from our type and add them to your custom type. Then in middleware config add type's key as a value of customOrderType optional property.

adyen: {
  location: '@vsf-enterprise/adyen-commercetools-api/server',
  configuration: {
    // ...
    customOrderType: 'keyOfMyCustomType'
  }
},

8

Setup webhook

Unfinished partial payments expire after some time. To elegantly handle it, we need to setup webhook to shoot in adyen integration's endpoint prepared for it.

Start from registering new webhook in Adyen's dashboard. URL should be equal:

https://my-alokai-frontend.local/api/adyen/webhookCancelOrder

Where adyen is equal a value of integrationName property from middleware config.

Method - JSON.
Encryption protocol - TLSv1.3.
Events - select only ORDER_CANCEL.
Additional settings - select only Include a success boolean for the payments listed in an ORDER_CLOSED event.

Save configuration.

Verifying HMAC signature

In order to prevent fraud requests to the endpoint, it is recommended to enable verifying HMAC signature. To do that, edit created webhook in Adyen's dashboard, Generate a new HMAC key, save configuration and copy generated key, as we will need it soon.

Then adjust a middleware config:

// ...
adyen: {
  location: '@vsf-enterprise/adyen-commercetools-api/server',
  configuration: {
    // ...
    enableHmacSignature: true,
    secretHmacKey: '<PASTE_HMAC_KEY_COPIED_FROM_ADYEN_DASHBOARD>'
  }
},
// ...

9

Setup commercetools-payment-to-order-processor

Some payment methods, like Klarna, certain 3DS authenticated cards, and Sofort, are redirect-based. This means there's a chance the user might lose connection or accidentally close the tab after payment but before returning to the return url. In such scenarios, a payment is successful, but no order is placed.

To address this issue, use the commercetools-payment-to-order-processor provided by commercetools. This service checks for the aforementioned situations at set intervals. If it identifies any matches, it will call a URL you specify, with the cartId attached as a query parameter. You must then set up an endpoint to create an order and provide the appropriate HTTP response. To understand this further, review the documentation in their repository.

Consider enhancing our middleware by adding an endpoint to create orders. Alternatively, you could design a straightforward Node.js service and deploy it as an auxiliary application in our cloud. Choose whichever approach you prefer, but ensure the endpoint is shielded from unauthorized access.