Redirecting to the Payment Provider

Redirect to a 3rd party site after the order is placed

Very often, after the shipping details are entered and the payment method is chosen on your website, the user needs to be redirected to a third-party site to complete the payment. You can easily do this in Scandi by writing a few plugins.

Note: In this example, we're implementing a specific payment method provider, Mollie. If you are making an extension for a different payment service, the steps outlined below might be slightly different. However, the general workflow of integrating a payment method will be similar: inspect the checkout source code and customize it through plugins.

Getting the Redirection URL

First, we need to get the URL that the user should be redirected to. For many payment extensions, this URL is included in the placeOrder GraphQL mutation response. Indeed, our example extension, Mollie, includes a mollie_redirect_url in the order field of the response.

However, as you may know, in GraphQL, only the fields that have been requested are found in the response. And of course, since mollie_redirect_url is an extension-specific field, it is not requested by default. Therefore, we need to write a plugin that will request this additional field as part of the placeOrder mutation.

After inspecting the Scandi source code, you'll find that the file responsible for creating the placeOrder mutation is scandipwa/src/query/Checkout.query.js.

All Scandi query creators live in the query folder. If you need to find the file responsible for a specific query, you can search for this query's name there.

First, let's take a look at the relevant parts of the originalCheckout.query file, to get an idea of what we're plugging in to:

scandipwa/src/query/Checkout.query.js
import { isSignedIn } from 'Util/Auth';
import { Field } from 'Util/Query';

/** @namespace Query/Checkout */
export class CheckoutQuery {
    // ...
    getPlaceOrderMutation(guestCartId) {
        const mutation = new Field('s_placeOrder')
            .setAlias('placeOrder')
            .addField(this._getOrderField());

        if (!isSignedIn()) {
            mutation.addArgument('guestCartId', 'String', guestCartId);
        }

        return mutation;
    }
		// ...

    _getOrderField() {
        return new Field('order')
            .addFieldList(['order_id']);
    }

    // ...
}

export default new CheckoutQuery();

As you see, the _getOrderField function is responsible for creating the order field, and populating it with the order_id subfield. We want to plug into this function to add an additional subfield – mollie_redirect_url.

So let's create a new plugin file! In the plugin directory of your extension, create a new Javascript file that ends in .plugin.js. It's a good idea to name it after the query it plugs into, so we'll name ours Checkout.query.plugin.js.

In Scandi, plugins are functions that wrap around the original function (read the documentation for more details of how they work). In this case, it's really simple:

plugin/Checkout.query.plugin.js
const _getOrderField = (args, callback, instance) => {
		// `callback` is the original function.
		// By calling it, we get the original `order` field,
		// which only contains 1 subfield, `order_id`, as seen above
    return callback()
        .addFieldList([
            'mollie_redirect_url', // we add the redirect URL field
            'mollie_payment_token' // and also this one (we'll need it later)
        ])
}

// This bit is the plugin configuration object.
export default {
		// First, specify the namespace to plug in to.
    "Query/Checkout": {
				// There are different types of plugins, but since we're
				// plugging into a member function (as opposed to static functions)
				// we specify the "member-function" type
        "member-function": {
						// There's only 1 plugin we want to specify here
            _getOrderField: _getOrderField
						// (But we could plug into other functions too if we wanted!)
        }
    }
}

If all goes smoothly, you should see a mollie_redirect_url value returned in the GraphQL response when completing an order with a Mollie payment method.

You can check the "Network" tab in your browser's developer tools to see the GraphQL responses. This can be useful for debugging requests!

Now, we have the redirect URL in the response, but we're not doing anything with it yet. The next step is to implement the actual redirection functionality.

Redirecting the User

First we need to find out which code is responsible for taking the user through the checkout steps. As the React Developer Tools extension will tell you, it's the Checkout route. We also know that the business logic (which is what we're interested in right now) most likely lives in the .container. So let's look at scandipwa/src/route/Checkout/Checkout.container.js , and the savePaymentMethodAndPlaceOrder function in particular:

scandipwa/src/route/Checkout/Checkout.container.js
// [imports...]
// [mapStateToProps and mapDispatchToProps]

/** @namespace Route/Checkout/Container */
export class CheckoutContainer extends PureComponent {
    // [...]

    async savePaymentMethodAndPlaceOrder(paymentInformation) {
        const { paymentMethod: { code, additional_data } } = paymentInformation;
        const guest_cart_id = !isSignedIn() ? getGuestQuoteId() : '';

        try {
            await fetchMutation(CheckoutQuery.getSetPaymentMethodOnCartMutation({
                guest_cart_id,
                payment_method: {
                    code,
                    [code]: additional_data
                }
            }));

            const orderData = await fetchMutation(CheckoutQuery.getPlaceOrderMutation(guest_cart_id));
            const { placeOrder: { order: { order_id } } } = orderData;

            this.setDetailsStep(order_id);
        } catch (e) {
            this._handleError(e);
        }
    }

    render() {...}
}

export default connect(mapStateToProps, mapDispatchToProps)(CheckoutContainer);

The function sets the payment method for the order, and places the order. After the order is placed, it simply transitions to the order success step (also known as the "details step").

We want to change this functionality – instead of transitioning to the details step, we need to redirect the user to the final payment step, on the 3rd party website. We already made sure that CheckoutQuery.getPlaceOrderMutation will return the redirection URL in the response, so we just need to use that.

Let's write a plugin for savePaymentMethodAndPlaceOrder that will redirect the user if necessary. We can add it to our existing Checkout.container.plugin.js file:

plugin/Checkout.container.plugin.js
import { redirectToUrl } from '../util/Redirect';
import { getPaymentToken, setPaymentToken } from '../util/PaymentTokenPersistence';

const savePaymentMethodAndPlaceOrder = async (args, callback, instance) => {
    const [paymentInformation] = args;

    const { paymentMethod: { code, additional_data } } = paymentInformation;
    const guest_cart_id = !isSignedIn() ? getGuestQuoteId() : '';

		// It's important to check if the user has selected a Mollie payment method.
		// We don't want to affect other methods, so we just use the callback directly in that case.
    if (!MOLLIE_METHODS.includes(code)) {
        return await callback(...args);
    }
		// Since this is a Mollie method, now we need some custom logic

    try {
				// Just like the original function, we set the payment method
        await fetchMutation(CheckoutQuery.getSetPaymentMethodOnCartMutation({
            guest_cart_id,
            payment_method: {
                code,
                [code]: additional_data,
            },
        }));

				// However, when placing the order, there is some additional data we need to consider
        const orderData = await fetchMutation(CheckoutQuery.getPlaceOrderMutation(guest_cart_id));
        const { placeOrder: { order: { mollie_redirect_url, mollie_payment_token } } } = orderData;

        if (Boolean(mollie_payment_token)) {
            // We'll need this token later, so let's save it to the browser's storage
            setPaymentToken(mollie_payment_token);
        } else {
						// It is a good idea to "fail early" if some data you expect to be present
						// is not there. Also, provide good error messages.
						// That way, your code will be easier to debug if something goes wrong.
            throw Error("Expected mollie_payment_token in order data, none found", orderData)
        }

				// We take the redirect URL we requested in the previous step and redirect to it
        if (Boolean(mollie_redirect_url)) {
            redirectToUrl(mollie_redirect_url)
        } else {
            throw Error("Expected mollie_redirect_url in order data, none found", orderData)
        }
    } catch (e) {
        instance._handleError(e);
    }
};

// Afain, we need to export the plugin configuration:
export default {
    "Route/Checkout/Container": {
        "member-function": {
            savePaymentMethodAndPlaceOrder,
        },
    },
}

As you might have noticed, we used a couple of custom utility functions, namely redirectToUrl and setPaymentToken. Of course we need to define them as well.

It's common for extensions to need new utility functions. They just need to implement them in the util directory, just like you would in a theme.

The redirectToUrl in Redirect.js is a really simple function that I found on StackOverflow (don't tell anyone). Still, it's nice to have it as a reusable function so the rest of the code is more readable.

util/Redirect.js
export const redirectToUrl = (url) => {
    window.location.replace(url);
};

The setPaymentToken and getPaymentToken functions (we haven't used the latter yet) are also very simple. They use the BrowserDatabase utility to save (and load) the payment token. This token will still be accessible after redirecting the user to the payment provider's page and back.

util/PaymentTokenPersistence.js
// Let's use Scandi's BrowserDatabase utility for persistence
import BrowserDatabase from 'Util/BrowserDatabase';
import { ONE_MONTH_IN_SECONDS } from 'Util/Request/QueryDispatcher';

const TOKEN_KEY = 'mollie_payment_token';

export const setPaymentToken = (token) => {
		// Again, some error checking can help catch mistakes early.
    if (!Boolean(token)) {
        throw Error("Must specify token to set")
    }

    BrowserDatabase.setItem(token, TOKEN_KEY, ONE_MONTH_IN_SECONDS);
};

export const getPaymentToken = () => {
    const token = BrowserDatabase.getItem(TOKEN_KEY);

    if (!Boolean(token)) {
        throw Error("No payment token found in browser database")
    }

    return token
};

And with that, the redirection step is complete! If you wish, you can complete an order using Mollie's payment methods to verify that you are indeed redirected.

Last updated