How we refactored our payment method abstractions with Vue.js 3

Vue.js

The refurbed Checkout application is a micro frontend and uses Vue.js in order to handle the complex interactivity, multiple payments systems, validations, and workflows.

Since the creation of the original Checkout in Vue.js, we handled each Payment system as the pattern described below:

  • stateful logic (e.g., if the payment is progressing or not) and payment provider information on Vuex
  • each single payment method type has its own component (e.g., Cards, Digital Wallet, etc.)
  • payment provider logic abstracted in specific services (Stripe, Braintree)
  • a Payment.vue component orchestrating all the logic

This solution has been good enough while we had a few payment methods, but it became limiting as soon as we needed to integrate more providers. In particular:

  • the main Payment component significantly grew every time a new provider was added, becoming difficult to reason about and extend
  • keeping the stateful logic regarding payments in Vuex required the Payment component to wire all the interactions
  • testing the component required mounting a Payment component and setup several Vuex bindings

We recently ported our Checkout application to Vue.js 3, which provided us with new patterns to structure and reuse code.

The refactoring

After analysing the problem, we decided to structure the whole Payment solution in a new way, in order that:

  • each payment method has its own handler, with its own implementation
  • each payment method has its own component, which setup the payment handler when the component is mounted
  • each payment method invokes the handler’s functions when requested
  • each payment handler is using EventEmitter to emit events, in order that both Payment.vue and child components can listen to them and make UI changes when required (e.g., loading animation, or calling another handler method when required)
  • a new component called PaymentController reacts to the switching of payments methods, setting up and displaying the selected method

The following diagram describes the procedure:

Payment Diagram

We noticed that Vue.js 3 composables were an ideal pattern to implement the handlers for each Payment method. In fact, the handlers exported methods are in that way more integrated:


setup() {
    ...

    const { start, pay, finalize } = useCardPaymentHandler()

    ...

    return {
        start,
        pay,
        finalize
    }
}

and easily callable from the Payment method.

As we mentioned, the handlers are emitting events, which are listened in some components in order to trigger UI changes. This is what happens in CardPayment.vue:


setup() {

    ...

    const isPaymentInProgress = ref(false);

    const {... , finalize } = useCardPaymentHandler();

    const handlePaymentSuccess = () => {
        isPaymentInProgress.value = false;
        finalize();
    }

    onMounted(() => {
        EventEmitter.on(PaymentEvents.PAYMENT_SUCCESS, handlePaymentSuccess);
    })

    onBeforeUnmount(() => {
        EventEmitter.off(PaymentEvents.PAYMENT_SUCCESS, handlePaymentSuccess);
    })

    ...

}

while the same event is listened, in Payment.vue, in order to unlock the read-only status that we set when a payment is in progress.

Conclusion

To conclude, the business benefits of this approach are the following ones:

  • adding new Payment methods becomes surprisingly trivial
  • the middle layers are now much simpler and maintainable

From the technical perspective:

  • the main Payment.vue component is just listening for events and displaying data common to all Payment methods, e.g., the alert when a payment has thrown an error
  • now each Vue.js component’s responsibility is to update the UI and call the proper methods, without having to wire up Vuex state

Written by

Stefano Gardano

November 22, 2022

Stefano is a Senior Frontend Engineer at refurbed.

We're Hiring

  • Senior Go Backend Developer (m/f/x)

    We are looking for a Senior Go Developer to help us build our platform. This includes our main API, AMQP worker daemon as well as several other services.

  • Senior Data Engineer (m/f/x)

    We are looking for a Senior Data Engineer to work in the intersection between engineering and data science. Help us improve our data processing workflows and push them to the next level.

  • Senior Vue.js Frontend Developer (m/f/x)

    We are looking for a Senior Vue.js Developer to support us in developing our external and internal interfaces. These include our checkout application, customer area and management interfaces for us and our merchants.

View all positions