Add a Login Popup in Magento 2

Rather than redirecting customers to a login page, many merchants favour showing a login popup enabling the user to login without having to navigate away from the current page. To add a login popup in Magento 2 requires knowledge of JavaScript and layout usage in Magento.

Magento already contains a vendor/magento/module-customer/view/frontend/web/js/model/authentication-popup.js file, which is used on the cart page if guest checkout is enabled and the user clicks the Proceed to Checkout button without being logged in.

Add a Login Popup in Magento 2

To re-use this popup on the cart and checkout pages, you can override the authentication-popup.js within your theme to include an additional class that triggers the popup.

To do this, copy the file into the app/design/frontend/[Vendor]/[theme]/Magento_Customer/web/js/model/authentication-popup.js directory, and add the additional class to the trigger option.


define([
    'jquery',
    'Magento_Ui/js/modal/modal'
], function ($, modal) {
    'use strict';

    return {
        modalWindow: null,

        /**
         * Create popUp window for provided element
         *
         * @param {HTMLElement} element
         */
        createPopUp: function (element) {
            var options = {
                'type': 'popup',
                'modalClass': 'popup-authentication',
                'focus': '[name=username]',
                'responsive': true,
                'innerScroll': true,
                'trigger': '.proceed-to-checkout, .some-other-class',
                'buttons': []
            };

            this.modalWindow = element;
            modal(options, $(this.modalWindow));
        },

        /** Show login popup window */
        showModal: function () {
            $(this.modalWindow).modal('openModal');
        }
    };
});

You may require a popup that isn't related to the cart and checkout pages, and doesn't contain text regarding checking out. Here is how the custom functionality to create a login popup should be held within a custom module.

Start by adding the registration.php and module.xml files.

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    '[Vendor]_[Module]',
    __DIR__
);
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="[Vendor]_[Module]" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Customer" />
        </sequence>
    </module>
</config>

Enable the module and upgrade the database by running the respective commands from the root directory of your Magento installation.

$ /path/to/php bin/magento module:enable [Vendor]_[Module]
$ /path/to/php bin/magento setup:upgrade

Within this module, a trigger-ajax-login class will be added to the Sign In link triggering the popup, and preventing the user from being redirected to the login page.

As the Sign In and Sign Out links are both rendered by the same template file, vendor/magento/module-customer/view/frontend/templates/account/link/authorization.phtml, the class needs to be added to just the Sign In link.

To do this, add an observer to the layout_load_before event, by defining it in an events.xml file located in the module's etc/frontend directory.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="layout_load_before">
        <observer name="add_ajaxlogin_loggedout_handle" instance="[Vendor]\[Module]\Observer\AddLoggedOutHandleObserver" />
    </event>
</config>

Now add the observer class which will add the custom layout handle for logged out customers only.

<?php
namespace [Vendor]\[Module]\Observer;

use Magento\Customer\Model\Session;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;

class AddLoggedOutHandleObserver implements ObserverInterface
{
    /**
     * @var Session
     */
    private $customerSession;

    /**
     * AddCustomerHandlesObserver constructor.
     *
     * @param Session $customerSession
     */
    public function __construct(
        Session $customerSession
    )
    {
        $this->customerSession = $customerSession;
    }

    /**
     * Add a custom handle responsible for adding the trigger-ajax-login class
     *
     * @param Observer $observer
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function execute(Observer $observer)
    {
        $layout = $observer->getEvent()->getLayout();

        if (!$this->customerSession->isLoggedIn()) {
            $layout->getUpdate()->addHandle('ajaxlogin_customer_logged_out');
        }
    }
}

Now create a ajaxlogin_customer_logged_out.xml layout file that will be responsible for adding the trigger-ajax-login CSS class.

The Sign In and Sign Out link blocks are named differently in the default Blank and Luma Magento themes. Depending on what parent your Magento theme child uses will depend on which <referenceBlock> you use.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <!-- Blank theme -->
        <referenceBlock name="authorization-link">
            <arguments>
                <argument name="class" xsi:type="string">trigger-ajax-login</argument>
            </arguments>
        </referenceBlock>

        <!-- Luma -->
        <referenceBlock name="authorization-link-login">
            <arguments>
                <argument name="class" xsi:type="string">trigger-ajax-login</argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

In addition to the above code, define the template and the JS component that will be used to render the login popup.

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <!-- Blank theme -->
        <referenceBlock name="authorization-link">
            <arguments>
                <argument name="class" xsi:type="string">trigger-ajax-login</argument>
            </arguments>
        </referenceBlock>

        <!-- Luma -->
        <referenceBlock name="authorization-link-login">
            <arguments>
                <argument name="class" xsi:type="string">trigger-ajax-login</argument>
            </arguments>
        </referenceBlock>

        <referenceContainer name="content">
            <block class="Magento\Customer\Block\Account\AuthenticationPopup" name="ajaxlogin-popup" as="ajaxlogin-popup"
                   before="-" template="[Vendor]_[Module]::account/ajaxlogin-popup.phtml">
                <arguments>
                    <argument name="jsLayout" xsi:type="array">
                        <item name="components" xsi:type="array">
                            <item name="ajaxLogin" xsi:type="array">
                                <item name="component" xsi:type="string">[Vendor]_[Module]/js/view/ajaxlogin-popup</item>
                                <item name="children" xsi:type="array">
                                    <item name="messages" xsi:type="array">
                                        <item name="component" xsi:type="string">Magento_Ui/js/view/messages</item>
                                        <item name="displayArea" xsi:type="string">messages</item>
                                    </item>
                                </item>
                            </item>
                        </item>
                    </argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

Start with adding the ajaxlogin-popup.phtml template file responsible for rendering the Knockout template file added later.

<?php

// @codingStandardsIgnoreFile

/** @var \Magento\Customer\Block\Account\AuthenticationPopup $block */
?>

<div id="ajaxlogin-popup" data-bind="scope:'ajaxLogin'" style="display: none;">
    <script>
        window.ajaxLogin = <?= /* @noEscape */ $block->getSerializedConfig() ?>;
    </script>
    <!-- ko template: getTemplate() --><!-- /ko -->
    <script type="text/x-magento-init">
        {
            "#ajaxlogin-popup": {
                "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?>
            }
        }
    </script>
</div>

Next, add the JS component defined within the layout XML file. There are a few key points to view here:

  • Additional JavaScript files, [Vendor]_[Module]/js/action/login and [Vendor]_[Module]/js/model/ajaxlogin-popup are defined. These will be added further down the post.
  • A [Vendor]_[Module]/ajaxlogin-popup template is defined
  • A setAjaxModelElement() method is defined, which is referenced in the Knockout template added in the next step.

define([
    'jquery',
    'ko',
    'Magento_Ui/js/form/form',
    '[Vendor]_[Module]/js/action/login',
    'Magento_Customer/js/customer-data',
    '[Vendor]_[Module]/js/model/ajaxlogin-popup',
    'mage/translate',
    'mage/url',
    'mage/validation'
], function ($, ko, Component, loginAction, customerData, ajaxLogin, $t, url) {
    'use strict';

    return Component.extend({
        registerUrl: window.ajaxLogin.customerRegisterUrl,
        forgotPasswordUrl: window.ajaxLogin.customerForgotPasswordUrl,
        autocomplete: window.ajaxLogin.autocomplete,
        modalWindow: null,
        isLoading: ko.observable(false),

        defaults: {
            template: '[Vendor]_[Module]/ajaxlogin-popup'
        },

        /**
         * Init
         */
        initialize: function () {
            var self = this;

            this._super();

            url.setBaseUrl(window.ajaxLogin.baseUrl);
            loginAction.registerLoginCallback(function () {
                self.isLoading(false);
            });
        },

        /** Init popup login window */
        setAjaxModelElement: function (element) {
            if (ajaxLogin.modalWindow == null) {
                ajaxLogin.createPopUp(element);
            }
        },

        /** Is login form enabled for current customer */
        isActive: function () {
            var customer = customerData.get('customer');

            return customer() == false; //eslint-disable-line eqeqeq
        },

        /** Show login popup window */
        showModal: function () {
            if (this.modalWindow) {
                $(this.modalWindow).modal('openModal');
            }
        },

        /**
         * Provide login action
         *
         * @return {Boolean}
         */
        login: function (formUiElement, event) {
            var loginData = {},
                formElement = $(event.currentTarget),
                formDataArray = formElement.serializeArray();

            event.stopPropagation();
            event.preventDefault();

            formDataArray.forEach(function (entry) {
                loginData[entry.name] = entry.value;
            });

            if (formElement.validation() &&
                formElement.validation('isValid')
            ) {
                this.isLoading(true);
                loginAction(loginData);
            }

            return false;
        }
    });
});

Now add the Knockout template, ensuring that the afterRender data-bind is set to the setAjaxModelElement() method as defined above.

Within this template, you can customise the look of your popup. For simplicity, the majority of the popup looks the same as Magento's proceed to checkout popup, apart from text changes.

<div class="block-authentication"
     data-bind="afterRender: setAjaxModelElement, blockLoader: isLoading"
     style="display: none">

    <div class="block block-new-customer"
         data-bind="attr: {'data-label': $t('or')}">
        <div class="block-title">
            <strong id="block-new-customer-heading"
                    role="heading"
                    aria-level="2"
                    data-bind="i18n: 'Create an Account'"></strong>
        </div>
        <div class="block-content" aria-labelledby="block-new-customer-heading">
            <p data-bind="i18n: 'Creating an account has many benefits:'"></p>
            <ul>
                <li data-bind="i18n: 'See order and shipping status'"></li>
                <li data-bind="i18n: 'Track order history'"></li>
                <li data-bind="i18n: 'Check out faster'"></li>
            </ul>
            <div class="actions-toolbar">
                <div class="primary">
                    <a class="action action-register primary" data-bind="attr: {href: registerUrl}">
                        <span data-bind="i18n: 'Create an Account'"></span>
                    </a>
                </div>
            </div>
        </div>
    </div>
    <div class="block block-customer-login"
         data-bind="attr: {'data-label': $t('or')}">
        <div class="block-title">
            <strong id="block-customer-ajaxlogin-heading"
                    role="heading"
                    aria-level="2"
                    data-bind="i18n: 'Login'"></strong>
        </div>
        <!-- ko foreach: getRegion('messages') -->
        <!-- ko template: getTemplate() --><!-- /ko -->
        <!--/ko-->
        <!-- ko foreach: getRegion('before') -->
        <!-- ko template: getTemplate() --><!-- /ko -->
        <!-- /ko -->
        <div class="block-content" aria-labelledby="block-customer-login-heading">
            <form class="form form-login"
                  method="post"
                  data-bind="event: {submit: login }"
                  id="ajaxlogin-form">
                <div class="fieldset login" data-bind="attr: {'data-hasrequired': $t('* Required Fields')}">
                    <div class="field email required">
                        <label class="label" for="ajaxlogin-email"><span data-bind="i18n: 'Email Address'"></span></label>
                        <div class="control">
                            <input name="username"
                                   id="ajaxlogin-email"
                                   type="email"
                                   class="input-text"
                                   data-bind="attr: {autocomplete: autocomplete}"
                                   data-validate="{required:true, 'validate-email':true}">
                        </div>
                    </div>
                    <div class="field password required">
                        <label for="ajaxlogin-pass" class="label"><span data-bind="i18n: 'Password'"></span></label>
                        <div class="control">
                            <input name="password"
                                   type="password"
                                   class="input-text"
                                   id="ajaxlogin-pass"
                                   data-bind="attr: {autocomplete: autocomplete}"
                                   data-validate="{required:true}">
                        </div>
                    </div>
                    <!-- ko foreach: getRegion('additional-login-form-fields') -->
                    <!-- ko template: getTemplate() --><!-- /ko -->
                    <!-- /ko -->
                    <div class="actions-toolbar">
                        <div class="primary">
                            <button type="submit" class="action action-login secondary" name="send" id="ajaxlogin-send">
                                <span data-bind="i18n: 'Sign In'"></span>
                            </button>
                        </div>
                        <div class="secondary">
                            <a class="action" data-bind="attr: {href: forgotPasswordUrl}">
                                <span data-bind="i18n: 'Forgot Your Password?'"></span>
                            </a>
                        </div>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>

As seen in the view/frontend/web/js/view/ajaxlogin-popup.js, we've got access to ajaxLogin via the view/frontend/web/js/model/ajaxlogin-popup.js file.

This file is used to handle the creation of the popup modal, as specified by the ajaxLogin.createPopUp(element); line.

The JavaScript model file can be seen below. Note the trigger option is set to the .trigger-ajax-login CSS class.


define([
    'jquery',
    'Magento_Ui/js/modal/modal'
], function ($, modal) {
    'use strict';

    return {
        modalWindow: null,

        /**
         * Create popUp window for provided element
         *
         * @param {HTMLElement} element
         */
        createPopUp: function (element) {
            var options = {
                'type': 'popup',
                'modalClass': 'popup-authentication',
                'focus': '[name=username]',
                'responsive': true,
                'innerScroll': true,
                'trigger': '.trigger-ajax-login',
                'buttons': []
            };

            this.modalWindow = element;
            modal(options, $(this.modalWindow));

        },
    };
});

Also seen within view/frontend/web/js/view/ajaxlogin-popup.js is the JavaScript login file, view/frontend/web/js/action/login.js.


define([
    'jquery',
    'mage/storage',
    'Magento_Ui/js/model/messageList',
    'Magento_Customer/js/customer-data'
], function ($, storage, globalMessageList, customerData) {
    'use strict';

    var callbacks = [],

        /**
         * @param {Object} loginData
         * @param {String} redirectUrl
         * @param {*} isGlobal
         * @param {Object} messageContainer
         */
        action = function (loginData, redirectUrl, isGlobal, messageContainer) {
            messageContainer = messageContainer || globalMessageList;

            return storage.post(
                'ajaxlogin/ajax/login',
                JSON.stringify(loginData),
                isGlobal
            ).done(function (response) {
                if (response.errors) {
                    messageContainer.addErrorMessage(response);
                    callbacks.forEach(function (callback) {
                        callback(loginData);
                    });
                } else {
                    callbacks.forEach(function (callback) {
                        callback(loginData);
                    });
                    customerData.invalidate(['customer']);

                    if (redirectUrl) {
                        window.location.href = redirectUrl;
                    } else if (response.redirectUrl) {
                        window.location.href = response.redirectUrl;
                    } else {
                        location.reload();
                    }
                }
            }).fail(function () {
                messageContainer.addErrorMessage({
                    'message': 'Could not authenticate. Please try again later'
                });
                callbacks.forEach(function (callback) {
                    callback(loginData);
                });
            });
        };

    /**
     * @param {Function} callback
     */
    action.registerLoginCallback = function (callback) {
        callbacks.push(callback);
    };

    return action;
});

One important point to take note off from the above file is the URL the login data is posted. Here the custom module uses a custom ajaxlogin/ajax/login route.

Define the routes using a routes.xml file.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="ajaxlogin" frontName="ajaxlogin">
            <module name="[Vendor]_[Module]" />
        </route>
    </router>
</config>

Now add the controller. The functionality is not too dissimilar from Magento's default ajax URL, . One of the differences includes the removal of the redirectUrl data in the $response. This prevents the page redirecting bases on the redirection settings in Stores -> Configuration -> Customer -> Customer Configuration.

<?php
namespace [Vendor]\[Module]\Controller\Ajax;

use Magento\Customer\Api\AccountManagementInterface;
use Magento\Framework\Exception\EmailNotConfirmedException;
use Magento\Framework\Exception\InvalidEmailOrPasswordException;
use Magento\Customer\Model\Account\Redirect as AccountRedirect;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\LocalizedException;

/**
 * Login controller
 *
 * @method \Magento\Framework\App\RequestInterface getRequest()
 * @method \Magento\Framework\App\Response\Http getResponse()
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class Login extends \Magento\Framework\App\Action\Action
{
    /**
     * @var \Magento\Framework\Session\Generic
     */
    protected $session;

    /**
     * @var \Magento\Customer\Model\Session
     */
    protected $customerSession;

    /**
     * @var AccountManagementInterface
     */
    protected $customerAccountManagement;

    /**
     * @var \Magento\Framework\Json\Helper\Data $helper
     */
    protected $helper;

    /**
     * @var \Magento\Framework\Controller\Result\JsonFactory
     */
    protected $resultJsonFactory;

    /**
     * @var \Magento\Framework\Controller\Result\RawFactory
     */
    protected $resultRawFactory;

    /**
     * @var AccountRedirect
     */
    protected $accountRedirect;

    /**
     * @var ScopeConfigInterface
     */
    protected $scopeConfig;

    /**
     * Initialize Login controller
     *
     * @param \Magento\Framework\App\Action\Context $context
     * @param \Magento\Customer\Model\Session $customerSession
     * @param \Magento\Framework\Json\Helper\Data $helper
     * @param AccountManagementInterface $customerAccountManagement
     * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory
     * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory
     */
    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Magento\Customer\Model\Session $customerSession,
        \Magento\Framework\Json\Helper\Data $helper,
        AccountManagementInterface $customerAccountManagement,
        \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory,
        \Magento\Framework\Controller\Result\RawFactory $resultRawFactory
    ) {
        parent::__construct($context);
        $this->customerSession = $customerSession;
        $this->helper = $helper;
        $this->customerAccountManagement = $customerAccountManagement;
        $this->resultJsonFactory = $resultJsonFactory;
        $this->resultRawFactory = $resultRawFactory;
    }

    /**
     * Login registered users and initiate a session.
     *
     * Expects a POST. ex for JSON {"username":"user@magento.com", "password":"userpassword"}
     *
     * @return \Magento\Framework\Controller\ResultInterface
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
     */
    public function execute()
    {
        $credentials = null;
        $httpBadRequestCode = 400;

        /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */
        $resultRaw = $this->resultRawFactory->create();
        try {
            $credentials = $this->helper->jsonDecode($this->getRequest()->getContent());
        } catch (\Exception $e) {
            return $resultRaw->setHttpResponseCode($httpBadRequestCode);
        }
        if (!$credentials || $this->getRequest()->getMethod() !== 'POST' || !$this->getRequest()->isXmlHttpRequest()) {
            return $resultRaw->setHttpResponseCode($httpBadRequestCode);
        }

        $response = [
            'errors' => false,
            'message' => __('Login successful.')
        ];

        try {
            $customer = $this->customerAccountManagement->authenticate(
                $credentials['username'],
                $credentials['password']
            );
            $this->customerSession->setCustomerDataAsLoggedIn($customer);
            $this->customerSession->regenerateId();
        } catch (EmailNotConfirmedException $e) {
            $response = [
                'errors' => true,
                'message' => $e->getMessage()
            ];
        } catch (InvalidEmailOrPasswordException $e) {
            $response = [
                'errors' => true,
                'message' => $e->getMessage()
            ];
        } catch (LocalizedException $e) {
            $response = [
                'errors' => true,
                'message' => $e->getMessage()
            ];
        } catch (\Exception $e) {
            $response = [
                'errors' => true,
                'message' => __('Invalid login or password.')
            ];
        }
        /** @var \Magento\Framework\Controller\Result\Json $resultJson */
        $resultJson = $this->resultJsonFactory->create();
        $result = $resultJson->setData($response);
        return $result;
    }
}

The final step is to modify the Sign In link's href attribute. Currently, if you were to click on the link, the popup would show, however the value in the href would redirect you to the customer/account/login page.

Fortunately, the Magento\Customer\Block\Account\AuthorizationLink block contain a public getHref() and therefore can be modified using an after plugin.

Therefore create a di.xml to define the plugin.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Customer\Block\Account\AuthorizationLink">
        <plugin name="modify_signin_href" type="[Vendor]\[Module]\Plugin\ModifySignInHrefPlugin" />
    </type>
</config>

The plugin class to add can be seen below. Note that the href is only changed if the customer is logged out. If the customer if logged in, we still want the customer to be redirected to the logout page upon clicking the Sign Out link.

<?php
namespace [Vendor]\[Module]\Plugin;

use Magento\Customer\Model\Context;

class ModifySignInHrefPlugin
{
    /**
     * Customer session
     *
     * @var \Magento\Framework\App\Http\Context
     */
    protected $httpContext;

    /**
     * ModifySignInHrefPlugin constructor.
     *
     * @param \Magento\Framework\App\Http\Context $httpContext
     */
    public function __construct(
        \Magento\Framework\App\Http\Context $httpContext
    )
    {
        $this->httpContext = $httpContext;
    }

    /**
     * @param \Magento\Customer\Block\Account\AuthorizationLink $subject
     * @param $result
     * @return string
     */
    public function afterGetHref(\Magento\Customer\Block\Account\AuthorizationLink $subject, $result)
    {
        if (!$this->isLoggedIn()) {
            $result = '#';
        }
        return $result;
    }

    /**
     * @return bool
     */
    public function isLoggedIn()
    {
        return $this->httpContext->getValue(Context::CONTEXT_AUTH);
    }
}

Clear the cache and you should be presented with a login popup upon clicking the Sign In top link!

Add a Login Popup in Magento 2

Note: This article is based on Magento Open Source version 2.2.