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:
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.