ScandiPWA
Create Magento AppCreate ScandiPWA AppUser ManualGitHub
  • Why Scandi
  • πŸš€Quick-start Guide
  • πŸ—ΊοΈRoadmap
  • Introduction to the Stack
    • CMA, CSA, and ScandiPWA
    • Challenges
  • Setting up Scandi
    • Storefront Mode Setup
      • Proxying requests to server
    • Magento Mode Setup
    • Existing Magento 2 setup
    • Magento Commerce Cloud setup
    • Updating to new releases
      • Storefront mode upgrade
      • Magento mode upgrade
      • CMA upgrade
      • CSA upgrade
      • Custom ScandiPWA composer dependency update
      • Local ScandiPWA Composer Package Setup
    • Docker Setup [deprecated]
      • Legacy Docker setup
      • Migrating to CMA & CSA
  • Developing with Scandi
    • Override Mechanism
      • Overriding JavaScript
        • Overriding classes
        • Overriding non-classes
      • Overriding Styles
      • Overriding the HTML / PHP
      • Parent Themes
    • Extensions
      • Creating an extension
      • Installing an extension
      • Migrating from 3.x to 4.x
      • Publishing an extension
      • Extension Terminology
    • Working With Magento
      • Magento troubleshooting
      • Working with Magento modules
      • Working with GraphQL
      • GraphQL Security
      • Working with "granular cache"
    • Developer Tools
      • Debugging in VSCode
      • ScandiPWA CLI
      • Configuring ESLint
      • CSA Commands
    • Deploying Your App
      • Build & Deploy Android app
      • Build & Deploy iOS app
  • Structure
    • Directory Structure
    • Building Blocks
      • Components
        • Styling Components
      • Routes
      • Redux Stores
      • GraphQL Queries
      • Global Styles
      • The Util Directory
      • Type Checking
    • Application assets
    • Code Style
      • JavaScript Code Style
      • SCSS Code Style
  • Tutorials
    • Customizing Your Theme
      • Styling
        • Customizing the Global Styles
        • Adding a New Font
        • Overriding a Components Styles
        • Extending a Component's Styles
      • Customizing JavaScript
        • Customizing the Footer Copyright
        • Adding a New Page
        • Adding a Section in My Account
        • Adding a Tab on the Product Page
        • Creating a New Redux Store
    • Payment Method Integration
      • Setting Up for Development
      • Redirecting to the Payment Provider
      • Handling the Customer's Return
    • Creating a Custom Widget
      • Scandi CMS System Overview
      • Creating a Magento Widget
      • Implementing the Rendering
    • Video Tutorials
      • #1 Setting up and talking theory
      • #2 Templating in React
      • #3 Overriding a file
      • #4 Styling the application
      • #5 Patterns of ScandiPWA
    • Dark Mode Extension
    • Deploying Native Apps
    • Product 3D Model Extension
      • Part 1: Magento 3D Model Uploads
      • Part 2: GraphQL API
      • Part 3: Scandi Frontend
    • Social Share, Full Extension Development
      • STEP-1 and 2 Creating Magento 2 Module
      • STEP-3 Backend Configurations Settings
      • STEP-4 Simple GraphQl and Resolver
      • STEP-5 Creating Extension, Base Redux Store
      • STEP-6 Extension plugins
      • STEP-7 GraphQL types, Helpers
      • STEP-8 Query Field and FieldList
      • STEP-9 render Plugins and MSTP Plugin, Component creation
      • STEP-10 SocialShare Component Development
      • STEP-11 SocialShare for CategoryPage
      • TASK-1 Changing LinkedIn to Twitter
      • STEP-12 Comments for Admin Users
      • STEP-13 Final, bugfixes
    • Accessing Magento 2 Controllers
      • STEP-1 Creating Magento 2 Module
      • STEP-2 - Create Magento 2 Frontend Route and Basic Controller
      • STEP-3 Accessing Magento 2 Controller, Bypassing ScandiPWA frontend
      • STEP-4 Creating ScandiPWA Extension with additional dependencies
      • STEP-5 Creating Plugin and Axios request
  • About
    • Support
    • Release notes
    • Technical Information
    • Data Analytics
    • Contributing
      • Installation from Fork
      • Repository structure
      • Code contribution process
      • Submitting an Issue
      • Publishing ScandiPWA
Powered by GitBook
On this page
  • Setting the Return URL
  • Processing the Order
  • Displaying the Result
  • Conclusion

Was this helpful?

  1. Tutorials
  2. Payment Method Integration

Handling the Customer's Return

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:

<?php
namespace ScandiTutorials\\MollieScandiConfig\\Setup;

use Magento\\Framework\\App\\Config\\ConfigResource\\ConfigInterface;
use Magento\\Framework\\Setup\\InstallDataInterface;
use Magento\\Framework\\Setup\\ModuleContextInterface;
use Magento\\Framework\\Setup\\ModuleDataSetupInterface;
use Mollie\\Payment\\Config;

class InstallData implements InstallDataInterface
{
    /** @var ConfigInterface */
    protected $config;

    public function __construct(ConfigInterface $config) {
        $this->config = $config;
    }

    public function install(
			ModuleDataSetupInterface $setup, ModuleContextInterface $context
		) {
       $setup->startSetup();
       $this->config->saveConfig(Config::GENERAL_USE_CUSTOM_REDIRECT_URL, true);
       $this->config->saveConfig(
           Config::GENERAL_CUSTOM_REDIRECT_URL,
           '{{secure_base_url}}checkout/mollie_result?order_id={{increment_id}}&mollie_payment_token={{payment_token}}&mollie_order_hash={{order_hash}}'
       );
        $setup->endSetup();
    }
}

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:

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

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

__construct(props) {plplu
        super.__construct(props);

        const {
            toggleBreadcrumbs,
            totals: {
                is_virtual
            }
        } = props;

        toggleBreadcrumbs(false);

        this.state = {
            isLoading: is_virtual,
            isDeliveryOptionsLoading: false,
            requestsSent: 0,
            paymentMethods: [],
            shippingMethods: [],
            shippingAddress: {},
            checkoutStep: is_virtual ? BILLING_STEP : SHIPPING_STEP,
            orderID: '',
            paymentTotals: BrowserDatabase.getItem(PAYMENT_TOTALS) || {},
            email: '',
            isGuestEmailSaved: false,
            isCreateUser: false,
            estimateAddress: {}
        };

        if (is_virtual) {
            this._getPaymentMethods();
        }
    }

    componentDidMount() {
        const {
            history,
            showInfoNotification,
            totals: {
                items = []
            }
        } = this.props;

        if (!items.length) {
            showInfoNotification(__('Please add at least one product to cart!'));
            history.push(appendWithStoreCode('/cart'));
        }
    }

    // [...]

    render() {...}
}

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

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 step
import { 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 specified
    if (!paymentToken) {
				// Not a return URL, just call the original function
        return callback(...args)
    }
		// Otherwise, we make sure to *not* call the original function
		// or else it would interfere with our logic

    toggleBreadcrumbs(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
const componentDidMount = (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.
        return callback(...args)
    }

    const paymentToken = getPaymentToken();
    const processTransactionMutation = MollieQuery.getProcessTransactionMutation(paymentToken);

		// Make the GraphQL mutation for processing the transaction
    fetchMutation(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:

query/Mollie.query.js
import { Field } from 'Util/Query';

/** @namespace Query/Mollie */
export class MollieQuery {
    getProcessTransactionMutation(paymentToken) {
        if (!paymentToken) {
            throw Error("The payment_token is required")
        }

        return new Field('mollieProcessTransaction')
            .addArgument('input', 'MollieProcessTransactionInput', { payment_token: paymentToken })
            .addFieldList(['paymentStatus', this._getCartField()])
    }

    _getCartField() {
        return new Field('cart').addField('id')
    }
}

export default new MollieQuery()

Finally, don't forget to update the .container plugin configuration:

export default {
    "Route/Checkout/Container": {
        "member-function": {
            __construct,
            componentDidMount,
            savePaymentMethodAndPlaceOrder,
        },
    },
}

Displaying the Result

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.
export const CREATED = 'CREATED';
export const PAID = 'PAID';
export const AUTHORIZED = 'AUTHORIZED';
export const CANCELED = 'CANCELED';
export const ERROR = 'ERROR';

export const ALL_SUCCESS = [CREATED, PAID, AUTHORIZED]

// Provides a human-readable, translated string describing the order status.
export const getStatusMessage = (status) => {
    switch (status) {
        case CREATED:
            return __('Your order has been created.');
        case PAID:
            return __('Your order has been paid.');
        case AUTHORIZED:
            return __('Your order has been authorized.');
        case CANCELED:
            return __('Your order has been canceled.');
        case ERROR:
            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 Step
export const MOLLIE_PROCESSING_STEP = 'MOLLIE_PROCESSING_STEP';

function renderMollieStep() {
    const { mollie: { isLoading, paymentStatus } = {}, orderID } = this.props;

		// Display a loader while the order is processing
    if (isLoading) {
        return <Loader isLoading/>
    }

		// If the order was a success, render the CheckoutSuccess component
		// It renders a "continue shopping" button and the order ID
    if (ALL_SUCCESS.includes(paymentStatus)) {
        return (
            <CheckoutSuccess
                orderID={ orderID }
            />
        );
    }

    return false;
}

// We need to plug into the `stepMap` so we can configure it to handle our custom step
const stepMap = (member, instance) => ({
    ...member,
    [MOLLIE_PROCESSING_STEP]: {
        title: __('Loading'),
        url: '/mollie_result',
        render: renderMollieStep.bind(instance),
        areTotalsVisible: false,
    },
});

// We also customize the title
const renderTitle = (args, callback, instance) => {
    const { checkoutStep, mollie: { isLoading, paymentStatus } = {} } = instance.props;

		// Only handle our custom step; use the original function for everything else
    if (checkoutStep !== MOLLIE_PROCESSING_STEP) {
        return callback(...args);
    }

    if (isLoading) {
        return (
            <h2 block="Checkout" elem="Title">
                { __("Loading, please wait") }
            </h2>
        );
    }

    const message = getStatusMessage(paymentStatus);

    return (
        <h2 block="Checkout" elem="Title">
            { message }
        </h2>
    );
};

// Plugin configuration as always.
export default {
    '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!

PreviousRedirecting to the Payment ProviderNextCreating a Custom Widget

Last updated 3 years ago

Was this helpful?