Handle the customer's return from a 3rd party site
After the customer has entered the payment details on the 3rd party page, they are returned to the e-commerce store. Often, there is additional logic required in this step. For example, you might have to fetch the transaction result to display it to the user.
In our case, we need to call the mollieProcessTransaction GraphQL mutation to make sure the transaction is processed, and to retrieve the order result (success/failure, etc.). Finally, we need to display this result to the user, in the form of a success or failure message.
Setting the Return URL
First, we need to ensure that the correct return URL is used. Since all Scandi checkout steps start with checkout, we're going to use checkout/mollie_result for the final order processing and displaying the result.
Mollie allows us to do this really easily by configuring some database values:
Now, we need to make sure that the correct business logic takes place after this redirect.
Processing the Order
When the user is redirected back, we need to use the mollieProcessTransaction GraphQL mutation to process the order and get the transaction result.
First, let's take a look at some relevant methods in the CheckoutContainer, which renders all checkout routes, and thus will be involved in checkout/mollie_result as well:
We'll need to change the behavior of __construct, because otherwise it would cause problems by setting an incorrect initial state β such as BILLING_STEP or SHIPPING_STEP β but we actually want a custom MOLLIE_PROCESSING_STEP to be set.
For this, we can write a plugin. The function is very similar to the original one, but sets the correct initial step if applicable:
plugin/Checkout.component.plugin.js
// We'll define this later...// MOLLIE_PROCESSING_STEP is a simple string that identifies the current checkout stepimport { MOLLIE_PROCESSING_STEP } from'./Checkout.component.plugin';const__construct= (args, callback, instance) => {const [props] = args;const {toggleBreadcrumbs, } = props;const { orderId,paymentToken,orderHash } =getParameters();// This is how we determine that it's a Mollie return URL:// The mollie_payment_token is specifiedif (!paymentToken) {// Not a return URL, just call the original functionreturncallback(...args) }// Otherwise, we make sure to *not* call the original function// or else it would interfere with our logictoggleBreadcrumbs(false);instance.state = { isLoading:false, isDeliveryOptionsLoading:false, requestsSent:0, paymentMethods: [], shippingMethods: [], shippingAddress: {}, checkoutStep:MOLLIE_PROCESSING_STEP,// Set a custom step orderID: orderId, paymentTotals:BrowserDatabase.getItem(PAYMENT_TOTALS) || {}, email:'', isGuestEmailSaved:false, isCreateUser:false, estimateAddress: {}, mollieParameters: { orderId, paymentToken, orderHash }, mollie: { isLoading:true }, };};
We'll also plug into componentDidMount, since that's a good place to put business logic that needs to run once when the component is initialized. The original function has some logic that we want to suppress for the Mollie processing step, so we'll check before calling it.
plugin/Checkout.component.plugin.js
constcomponentDidMount= (args, callback, instance) => {const { mollieParameters: { paymentToken: isMolliePayment } = {} } =instance.state;// Check if it's a Mollie Payment return URL.if (!isMolliePayment) {// If not, just call the original function.returncallback(...args) }constpaymentToken=getPaymentToken();constprocessTransactionMutation=MollieQuery.getProcessTransactionMutation(paymentToken);// Make the GraphQL mutation for processing the transactionfetchMutation(processTransactionMutation).then(({ mollieProcessTransaction: { paymentStatus, cart } }) => {instance.setState({ mollie: { isLoading:false, paymentStatus, cart, }, }) }).catch((e) => {console.error(e);instance.setState({ mollie: { isLoading:false, paymentStatus:ERROR, cart:null, }, }) })};
The above plugin makes use of the MollieQuery class. That's a query creator utility for mollieProcessTransaction which we also have to define:
After the order has been processed, we need to inform the user of the order status.
util/PaymentStatus.js
// This list was found in Mollie's backend code.// We define the possible statuses so we can reuse them.exportconstCREATED='CREATED';exportconstPAID='PAID';exportconstAUTHORIZED='AUTHORIZED';exportconstCANCELED='CANCELED';exportconstERROR='ERROR';exportconstALL_SUCCESS= [CREATED,PAID,AUTHORIZED]// Provides a human-readable, translated string describing the order status.exportconstgetStatusMessage= (status) => {switch (status) {caseCREATED:return__('Your order has been created.');casePAID:return__('Your order has been paid.');caseAUTHORIZED:return__('Your order has been authorized.');caseCANCELED:return__('Your order has been canceled.');caseERROR:return__('There was an error processing your order.') }};
Now, we can add plugins for the Checkout .component, which is responsible for the presentation logic of the checkout flow.
import { ALL_SUCCESS, getStatusMessage } from'../util/PaymentStatus';import CheckoutSuccess from'Component/CheckoutSuccess';import Loader from'Component/Loader';// A string representing the Mollie Processing StepexportconstMOLLIE_PROCESSING_STEP='MOLLIE_PROCESSING_STEP';functionrenderMollieStep() {const { mollie: { isLoading,paymentStatus } = {},orderID } =this.props;// Display a loader while the order is processingif (isLoading) {return <LoaderisLoading/> }// If the order was a success, render the CheckoutSuccess component// It renders a "continue shopping" button and the order IDif (ALL_SUCCESS.includes(paymentStatus)) {return ( <CheckoutSuccessorderID={ orderID } /> ); }returnfalse;}// We need to plug into the `stepMap` so we can configure it to handle our custom stepconststepMap= (member, instance) => ({...member, [MOLLIE_PROCESSING_STEP]: { title:__('Loading'), url:'/mollie_result', render:renderMollieStep.bind(instance), areTotalsVisible:false, },});// We also customize the titleconstrenderTitle= (args, callback, instance) => {const { checkoutStep, mollie: { isLoading,paymentStatus } = {} } =instance.props;// Only handle our custom step; use the original function for everything elseif (checkoutStep !==MOLLIE_PROCESSING_STEP) {returncallback(...args); }if (isLoading) {return ( <h2block="Checkout"elem="Title"> { __("Loading, please wait") } </h2> ); }constmessage=getStatusMessage(paymentStatus);return ( <h2block="Checkout"elem="Title"> { message } </h2> );};// Plugin configuration as always.exportdefault {'Route/Checkout/Component': {'member-property': { stepMap, },'member-function': { renderTitle, }, },}
Conclusion
At this point, we've covered the main steps in integrating a payment method extension in Scandi. Usually, you need to do some debugging to make sure everything is working properly β payment integration can be tricky! You should be able to test your extension using the test mode of the payment provider.
When you're done with the extension, consider publishing it on the ScandiPWA Marketplace so others can benefit from your creation!