Upgrading to 4.0.0

Introduction

Changelog is available here.

Requirements

  • commercetools integration ^1.10.0.

Support for retry if user cancelled redirect-based payment

The feature requires dedicated view to which user will be redirected and additional configuration inside middleware.config.js.

Adding dedicated view

In our application, the session cookie (named vsf-commercetools-token by default) is in Strict mode. Because of that, it won't be available if the user is being redirected back from 3rd party website. However, the Strict mode is essential for safety as the mechanism protects users from CSRF attacks. That's why, in case of error, we need to redirect user to the dedicated view inside our application and allow to move manually from there.

We've prepared an example view for that purpose (and other errors that might appear in redirect-based flow). You can use it by creating a new file pages/AdyenPaymentError.vue with the following content:

<template>
  <section id="adyen-payment-error">
    <SfCallToAction
      class="banner"
      :title="$t('Payment error!')"
      :image="'/thank-you/bannerD.webp' | addBasePathFilter"
    >
      <template #description>
        <span v-if="error === 'malformed-price'">{{ $t('We stopped the payment process as it looks like the total price in your cart changed since the payment was initiated. Please provide payment data once again and we will create a payment request with the updated price.') }}</span>
        <span v-else-if="error === 'cancelled-transaction'">{{ $t('We see you cancelled the payment process. Feel free to continue shopping or click button below to come back to the payment page.') }}</span>
        <span v-else-if="error === 'creating-order-failed'">{{ $t("Unfortunately, for some reason we couldn't make an order after succesful payment. We sent refund request. Please try again with different payment method. If problem appears again, please contact the website's administrator.") }}</span>
        <span v-else-if="error === 'unprocessable-entity'">{{ $t("Unfortunately, for some reason the payment service provider couldn't process the payment. Please try again with different payment method. If problem appears again, please contact the website's administrator.") }}</span>
        <span v-else-if="error === 'payment-error'">{{ $t('The payment process failed. Feel free to continue shopping or click button below to come back to the payment page.') }}</span>
        <span v-else>{{ $t('Some unexpected error appeared. Feel free to continue shopping or click button below to come back to the payment page. Please use different payment method next time or contact website\'s administrator if it happens again.') }}</span>
      </template>
    </SfCallToAction>
    <SfButton
      link="checkout/payment"
      class="sf-button back-button color-secondary button-size"
    >
        {{ $t('Go to payment page') }}
      </SfButton>
  </section>
</template>

<script>
import { SfButton, SfCallToAction } from '@storefront-ui/vue';
import { computed, useRoute } from '@nuxtjs/composition-api';

export default {
  name: 'AdyenPaymentError',
  components: {
    SfButton,
    SfCallToAction
  },
  setup () {
    const route = useRoute();
    const error = computed(() => route.value.query.error);

    return {
      error
    };
  }
};
</script>

<style lang="scss" scoped>
#adyen-payment-error {
  box-sizing: border-box;
  @include for-desktop {
    max-width: 1240px;
    padding: 0;
    margin: 0 auto;
  }
}

.banner {
  --call-to-action-color: var(--c-text);
  --call-to-action-title-font-size: var(--h2-font-size);
  --call-to-action-title-font-weight: var(--font-weight--semibold);
  --call-to-action-text-container-width: 50%;
  @include for-desktop {
    margin: 0 0 var(--spacer-xl) 0;
  }
  &__order-number {
    display: flex;
    flex-direction: column;
    font: var(--font-weight--light) var(--font-size--sm) / 1.4
      var(--font-family--primary);
    @include for-desktop {
      flex-direction: row;
      font-size: var(--font-size--normal);
    }
  }
}

.back-button {
  --button-width: calc(100% - var(--spacer-lg));
  margin: 0 auto var(--spacer-base) auto;
  @include for-desktop {
    margin: var(--spacer-xl) auto;
    &:hover {
      color: var(--c-white);
    }
  }
}
.button-size {
  @include for-desktop {
    --button-width: 25rem;
  }
}
</style>

And then adding a new route in the nuxt.config.js, add a new argument to the routes.push call inside router.extendRoutes:












 
 
 
 
 






export default {
  // ...
  router: {
    middleware: [],
    extendRoutes(routes, resolve) {
      routes.push(
        {
          name: 'home',
          path: '/',
          component: resolve(__dirname, 'pages/Home.vue')
        },
        {
          name: 'adyen-payment-error',
          path: '/adyen-payment-error',
          component: resolve(__dirname, 'pages/AdyenPaymentError.vue')
        },
      );
    }
  }
  // ...
}

Then the user will be able to easily come back to the payment page if wants to. Cancelled Adyen payment view

Updating configuration

Now, let's configure the integration to redirect to newly created route. In order to that, we need to update buildRedirectUrlAfterError function inside middleware.config.js. It has to return a path to the route.













 
 
 
 



// middleware.config.js
module.exports = {
  integrations: {
    // ...
    adyen: {
      location: '@vsf-enterprise/adyen-commercetools/server',
      configuration: {
        ctApi: {
          // ...
        },
        adyenMerchantAccount: "VSFAccount243ECOM",
        origin: "http://localhost:3000",
        buildRedirectUrlAfterError: (err) => '/adyen-payment-error',
        buildRedirectUrlAfterAuth: (paymentAndOrder) => `/checkout/thank-you?order=${paymentAndOrder.order.id}`,
      }
    }
  },
};

Thanks to above:

  • when the user clicks Cancel in redirect-based payment method like Klarna then the application will redirect to the /adyen-payment-error?error=cancelled-transaction route,
  • when the user modified cart from another browser's tab in redirect-based payment method then the application will redirect to the /adyen-payment-error?error=malformed-price route,
  • when the order creation process fails for some reason in redirect-based payment method then the application will redirect to the /adyen-payment-error?error=creating-order-failed route,
  • when the payment fails instantly in redirect-based payment method then the application will redirect to the /adyen-payment-error?error=payment-error route,
  • when Adyen responds with 422 HTTP Code in redirect-based payment method then the application will redirect to the /adyen-payment-error?error=unprocessable-entity route.

Alternative approach

We do not recommend this approach as it might open a possibility to the CSRF attack. Use it only on own responsibility.

If you want, you can modify mode of vsf-commercetools-token cookie to the Lax. Thanks to that, you could redirect directly to the /checkout/payment view by returning it from the buildRedirectUrlAfterError. If you want to go for this approach, you can do it by adding cookie_options inside middleware.config.js. What's important, you need to do it for ct integration, not adyen.








 
 
 
 
 
 













// middleware.config.js
module.exports = {
  integrations: {
    ct: {
      location: '@vsf-enterprise/commercetools-api/server',
      configuration: {
        // ...
        cookie_options: {
          'vsf-commercetools-token': {
            path: "/",
            sameSite: "lax"
          }
        },
      },
      customQueries: ctCustomQueries
    },

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

Deprecated buildRedirectUrlIfMalformedPrice, buildRedirectUrlAfter3ds1Auth, and buildRedirectUrlAfter3ds1Error

In this version, we deprecated buildRedirectUrlIfMalformedPrice, buildRedirectUrlAfter3ds1Auth, and buildRedirectUrlAfter3ds1Error configuration properties. Please migrate to new names inside your middleware.config.js, accordingly:

-buildRedirectUrlAfter3ds1Auth: (paymentAndOrder) => `/checkout/thank-you?order=${paymentAndOrder.order.id}`,
+buildRedirectUrlAfterAuth: (paymentAndOrder) => `/checkout/thank-you?order=${paymentAndOrder.order.id}`,
-buildRedirectUrlIfMalformedPrice: () => '/adyen-payment-error',
-buildRedirectUrlAfter3ds1Error: () => '/adyen-payment-error',
+buildRedirectUrlAfterError: (err) => '/adyen-payment-error',

The signature of no method has changed.

Updating Vue component

There were a few changes in the Vue component called PaymentAdyenProvider. If you forked it, make sure to apply the following changes.

  1. Find:
<slot name="payment-refused-by-beforePay" v-else-if="isRefusedByBeforePay">
  <div class="payment-error-container">
    {{ $t('The payment has been refused. Please retry and make sure provided payment\'s details are correct.') }}
  </div>
</slot>

And add the following div below:

<div class="payment-error-container" v-else-if="errorMessage && errorMessage.length > 0">
  {{ errorMessage }}
</div>
  1. Adjust import:
-import { useAdyen, loadScript, ERROR_RESULT_CODES, MALFORMED_PRICE_ERROR } from '@vsf-enterprise/adyen-commercetools';
+import { useAdyen, loadScript, ERROR_RESULT_CODES, MALFORMED_PRICE_ERROR, UNPROCESSABLE_ENTITY_ERROR } from '@vsf-enterprise/adyen-commercetools';
  1. Add a new ref and functions inside setup function:
const errorMessage = ref('');

/**
 * It can be called only before the finalization of payment
 */
const reinitPayment = async () => {
  dropinInstance.value.unmount();
  await initCheckout();
};
const setErrorMessage = (msg) => {
  errorMessage.value = msg;
};
  1. Replace occurences of:
dropinInstance.value.unmount();
await initCheckout();

With:

await reinitPayment();
  1. Inside checkResponse function add a case for UNPROCESSABLE_ENTITY_ERROR by replacing:
if (error.value[action].data?.message === MALFORMED_PRICE_ERROR) {
  return await onMalformedPrice(true);
} else if (ERROR_RESULT_CODES.includes(error.value[action].resultCode)) {
  return await onMalformedPrice(false);
}

With:




 
 
 




const errorMessage = error.value[action].data?.message;
if (errorMessage === MALFORMED_PRICE_ERROR) {
  return await onMalformedPrice(true);
} else if (errorMessage === UNPROCESSABLE_ENTITY_ERROR) {
  setErrorMessage("We couldn't process your payment. Please try again with a different payment method. If it doesn't help, please contact website's administrator.");
  return await reinitPayment();
} else if (ERROR_RESULT_CODES.includes(error.value[action].resultCode)) {
  return await onMalformedPrice(false);
}
  1. Add reinitPayment and setErrorMessage to object passed to the buildDropinConfiguration.


 
 



const dropinConfiguration = buildDropinConfiguration({
  paymentMethodsResponse,
  reinitPayment,
  setErrorMessage,
  // ...
});
  1. Export errorMessage from setup function:









 



setup () {
  // ...
  return {
    dropinDiv,
    cart,
    error,
    isMalformed,
    isRefused,
    isRefusedByBeforePay,
    errorMessage
  };
}