Viewing File: /usr/local/cpanel/base/frontend/jupiter/domains/directives/domainSearchModalDirective.js

//                                      Copyright 2026 WebPros International, LLC
//                                                           All rights reserved.
// copyright@cpanel.net                                         http://cpanel.net
// This code is subject to the cPanel license. Unauthorized copying is prohibited.

/* global define */

/** @namespace cpanel.domains.directive.domainSearchModal */

define(
    [
        "angular",
        "lodash",
        "cjt/util/locale",
        "uiBootstrap",
        "app/services/domains",
        "app/directives/tldPriorityInputDirective",
        "cjt/services/alertService",
    ],
    function(angular, _, LOCALE) {

        "use strict";

        var MODULE_NAMESPACE = "cpanel.domains.domainSearchModal";

        var module = angular.module(MODULE_NAMESPACE, [
            "ui.bootstrap",
            "cpanel.domains.domains.service",
            "cpanel.domains.tldPriorityInput",
            "cjt2.services.alert",
        ]);

        var TEMPLATE_URL = "directives/domainSearchModal.ptt";
        var WINDOW_CLASS = "domain-search-modal-dialog";

        // Modal controller — extracted from the previous inline controller in
        // views/manageDomain.js so it can be reused in two places (the manage
        // recommendations banner and the create-domain "purchase" chooser).
        var ModalController = ["$scope", "$uibModalInstance", "$sce", "$timeout", "$document", "domains", "alertService", "options",
            function($modalScope, $uibModalInstance, $sce, $timeout, $document, $domainsService, $alertService, options) {

                options = options || {};
                var cachedExploreData = options.cachedExploreData || null;
                var exploreDataPromise = options.exploreDataPromise || null;

                // "create" = opened from the create-domain flow;
                // "manage" = opened from the manage-domain banner.
                var _source = options.source || "unknown";

                // Optional host callback invoked with the just-purchased
                // domain when the user chooses to add it to cPanel from the
                // post-redirect success panel. Lets the modal hand the domain
                // back to its caller so the caller can open its own add-domain
                // flow with the name prefilled, mirroring the "Registered
                // Domain" workflow. When absent, the success panel offers
                // only "Done".
                var _onAddDomain = typeof options.onAddDomain === "function"
                    ? options.onAddDomain
                    : null;
                $modalScope.canAddDomain = !!_onAddDomain;

                $modalScope.loading = false;
                $modalScope.searching = false;
                $modalScope.searchQuery = options.initialQuery || "";
                $modalScope.showSearchOptions = false;

                // Distinguish "caller seeded the filters" (e.g. the
                // create-domain chooser, which passes its own
                // initialTldFilters — already pre-populated from NVData on
                // that page) from "caller left them unset" (e.g. the
                // manage-domain recommendations banner). Only in the latter
                // case do we load the user's saved prioritisation here, so we
                // never clobber a selection the caller explicitly handed us.
                var _callerSeededTldFilters =
                    Object.prototype.hasOwnProperty.call(options, "initialTldFilters");
                $modalScope.tldFilters = _callerSeededTldFilters && angular.isArray(options.initialTldFilters)
                    ? options.initialTldFilters.slice()
                    : [];

                // Pre-populate the "Prioritize TLDs" control from the user's
                // persisted NVData preference so a prioritisation set in a
                // previous session carries over and is applied to the next
                // search the user runs (CPANEL-53293).
                //
                // The load is async, so track it: a search fired before it
                // settles must wait (see searchDomains), otherwise it would
                // run with an empty set and persist that empty set back,
                // clearing the saved preference.
                var _savedTldFiltersLoad = null;
                var _savedTldFiltersLoaded = _callerSeededTldFilters;

                if (!_callerSeededTldFilters) {
                    _savedTldFiltersLoad = $domainsService.getPrioritizedTlds()
                        .then(function(savedTlds) {
                            if (savedTlds && savedTlds.length && !$modalScope.tldFilters.length) {
                                $modalScope.tldFilters = savedTlds;
                            }
                        })
                        .finally(function() {
                            _savedTldFiltersLoaded = true;
                        });
                }

                $modalScope.searchPerformed = false;
                // True only after a search resolves with no featured or
                // available results, so the title can switch from
                // "We Think You'll Like …" to "No Domains Found".
                $modalScope.noResults = false;
                $modalScope.featured = cachedExploreData ? cachedExploreData.featured : [];
                $modalScope.available = cachedExploreData ? cachedExploreData.available : [];

                // The modal can be opened while the banner's recommendations
                // request is still in flight. When that happens there is no
                // cached explore data yet, so show the loading skeleton until
                // the same promise resolves rather than rendering an empty view.
                if (!cachedExploreData && exploreDataPromise) {
                    $modalScope.loading = true;
                    exploreDataPromise
                        .then(function(exploreData) {

                            // If the user already started a manual search while
                            // the initial request was in flight, keep their
                            // results instead of overwriting them.
                            if ($modalScope.searchPerformed) {
                                return;
                            }
                            exploreData = exploreData || {};
                            $modalScope.featured = exploreData.featured || [];
                            $modalScope.available = exploreData.available || [];
                        })
                        .catch(function() {
                            // Banner fetch failed; leave the lists empty so the
                            // user can still search manually.
                        })
                        .finally(function() {
                            $modalScope.loading = false;
                        });
                }

                $modalScope.matchedDomain = null;
                $modalScope.unavailableDomain = null;
                $modalScope.pendingSearchDomain = null;

                // null = still loading; [] = loaded but empty / unavailable
                $modalScope.supportedTlds = null;

                var storeConfig = null;

                var _storeConfigReady = $domainsService.fetchStoreConfig()
                    .then(function(config) {
                        storeConfig = config;
                    })
                    .catch(function() {
                        storeConfig = {};
                    });

                function _getMode() {
                    return (storeConfig &&
                            storeConfig.store_type &&
                            storeConfig.store_type.toLowerCase() === "whmcs")
                        ? "whmcs"
                        : "unknown";
                }

                function _trackEvent(eventName, props) {
                    if (window.mixpanel && typeof window.mixpanel.track === "function") {
                        window.mixpanel.track(eventName, props || {});
                    }
                }

                $domainsService.fetchSupportedTlds()
                    .then(function(tlds) {
                        $modalScope.supportedTlds = tlds;
                    })
                    .catch(function() {
                        $modalScope.supportedTlds = [];
                    });

                $modalScope.highlightTld = function(domain) {
                    var dotIndex = domain.indexOf(".");
                    if (dotIndex === -1) {
                        return $sce.trustAsHtml(_.escape(domain));
                    }
                    var name = domain.substring(0, dotIndex);
                    var tld = domain.substring(dotIndex);
                    return $sce.trustAsHtml(_.escape(name) + "<strong>" + _.escape(tld) + "</strong>");
                };

                $modalScope.searchDomains = function() {
                    if (!$modalScope.searchQuery || $modalScope.searching) {
                        return;
                    }

                    // Defer until the saved-TLD preference has loaded, so the
                    // first search on the manage-domain banner path (which
                    // opens the modal without seeded filters) applies and
                    // persists the saved filters instead of racing ahead with
                    // an empty set and clearing the stored value (CPANEL-53293).
                    if (!_savedTldFiltersLoaded && _savedTldFiltersLoad) {
                        _savedTldFiltersLoad.finally(function() {
                            $modalScope.searchDomains();
                        });
                        return;
                    }

                    $modalScope.searching = true;
                    $modalScope.searchPerformed = true;

                    var trimmedQuery = $modalScope.searchQuery.trim();

                    // A manual search supersedes the initial in-flight load, so
                    // clear its skeleton even if that request has not resolved.
                    $modalScope.loading = false;

                    _storeConfigReady.finally(function() {
                        _trackEvent("Domain-Search-Started", {
                            source: _source,
                            query: trimmedQuery,
                            mode: _getMode(),
                        });
                    });

                    var filterTlds = $modalScope.tldFilters.length
                        ? $modalScope.tldFilters
                        : null;

                    // Persist the prioritisation that is actually being
                    // searched so it survives across sessions (CPANEL-53293).
                    // Saving on search (rather than on every keystroke) keeps
                    // the stored value in step with what the user committed to,
                    // and an empty set clears the saved preference. Fire and
                    // forget — the service logs and swallows any write error so
                    // a persistence hiccup never blocks the search.
                    $domainsService.setPrioritizedTlds($modalScope.tldFilters);

                    var rawQuery = trimmedQuery.toLowerCase();
                    var queryHasTld = rawQuery.indexOf(".") !== -1;
                    $modalScope.pendingSearchDomain = rawQuery;

                    $domainsService.fetchRecommendations(trimmedQuery, filterTlds)
                        .then(function(result) {
                            $modalScope.matchedDomain = null;
                            $modalScope.unavailableDomain = null;
                            var featured = result.explore.featured || [];
                            var available = result.explore.available || [];

                            if (queryHasTld) {
                                var findMatch = function(list) {
                                    for (var i = 0; i < list.length; i++) {
                                        if (list[i] && list[i].domain &&
                                                list[i].domain.toLowerCase() === rawQuery) {
                                            return i;
                                        }
                                    }
                                    return -1;
                                };

                                var fIdx = findMatch(featured);
                                if (fIdx !== -1) {
                                    $modalScope.matchedDomain = featured[fIdx];
                                    featured = featured.slice(0, fIdx).concat(featured.slice(fIdx + 1));
                                } else {
                                    var aIdx = findMatch(available);
                                    if (aIdx !== -1) {
                                        $modalScope.matchedDomain = available[aIdx];
                                        available = available.slice(0, aIdx).concat(available.slice(aIdx + 1));
                                    }
                                }
                            }

                            if ($modalScope.matchedDomain) {
                                // Match found: success banner replaces featured cards;
                                // any "featured" recs roll into the "Also available" list.
                                available = featured.concat(available);
                                featured = [];
                            } else if (queryHasTld) {
                                // User searched a fully-qualified domain that
                                // isn't available: surface the "We're sorry"
                                // header and keep the featured suggestions
                                // as alternatives.
                                $modalScope.unavailableDomain = rawQuery;
                            }

                            $modalScope.featured = featured;
                            $modalScope.available = available;
                        })
                        .catch(function() {
                            // Clear any prior search's results so a failed
                            // search collapses to the empty "No Domains Found"
                            // state (computed in finally) instead of leaving the
                            // previous success/unavailable banner and lists on
                            // screen alongside the error alert.
                            $modalScope.matchedDomain = null;
                            $modalScope.unavailableDomain = null;
                            $modalScope.featured = [];
                            $modalScope.available = [];
                            $alertService.add({
                                type: "danger",
                                message: LOCALE.maketext("The domain search failed. Try again."),
                            });
                        })
                        .finally(function() {
                            $modalScope.searching = false;
                            $modalScope.pendingSearchDomain = null;
                            // Compute here, not in .then, so a failed search
                            // (e.g. missing API credentials) also flips the
                            // title to "No Domains Found" instead of leaving
                            // the stale "We Think You'll Like …" heading.
                            $modalScope.noResults =
                                !$modalScope.featured.length &&
                                !$modalScope.available.length;
                        });
                };

                $modalScope.handleSearchKeydown = function($event) {
                    if ($event.key === "Enter") {
                        $modalScope.searchDomains();
                    }
                };

                $modalScope.toggleSearchOptions = function() {
                    $modalScope.showSearchOptions = !$modalScope.showSearchOptions;
                };

                $modalScope.purchasingDomain = null;
                $modalScope.purchaseConfirmation = null;
                // Toggles the "Redirection Failed" overlay (Figma node
                // 86:10861) that sits on top of the confirmation panel when
                // the purchase-URL fetch fails.
                $modalScope.redirectFailed = false;
                // When the browser silently blocks window.open() (popup
                // blocker fires because the user-gesture context was lost
                // across the async fetchDomainPurchaseUrl boundary), this
                // holds the URL so the template can surface a clickable
                // fallback link. A direct anchor click IS a fresh user
                // gesture and is not blocked.
                $modalScope.blockedPurchaseUrl = null;

                // Called when the user clicks the fallback "Continue to
                // Store" link inside the "Redirecting to Store"
                // overlay. The click is a fresh user gesture that opens
                // the WHMCS cart in a new tab; once that handoff is in
                // flight, swap the overlay to the post-redirect success
                // state (CPANEL-53285) so the modal mirrors the direct
                // success path — the user gets the same "Redirected to
                // Store" panel whether the browser allowed the popup
                // or they had to click the fallback link.
                //
                // Two sequential $timeout ticks are used so the steps
                // settle into separate digests:
                //
                //   1. The first tick clears blockedPurchaseUrl. This
                //      removes the banner (which contains the anchor)
                //      via ng-if, but ONLY after the browser's native
                //      target="_blank" navigation has fired — a
                //      synchronous mutation would detach the anchor
                //      mid-click and could suppress the new tab in some
                //      browsers.
                //   2. The second tick calls _showRedirectSuccess(),
                //      which sets redirectSucceeded = true. That swaps
                //      the "Redirecting to Store" overlay (its ng-if
                //      depends on !redirectSucceeded) for the success
                //      overlay in a fresh digest, and queues the
                //      heading focus on a third tick. Splitting the
                //      overlay swap off prevents it from coinciding
                //      with the banner's ng-if teardown; chaining the
                //      mutations across two digests is the most
                //      defensive ordering against AngularJS render-time
                //      surprises.
                $modalScope.dismissBlockedPurchaseUrl = function() {
                    $timeout(function() {
                        $modalScope.blockedPurchaseUrl = null;
                        $timeout(function() {
                            _showRedirectSuccess();
                        });
                    });
                };

                // Toggles the post-redirect success overlay (CPANEL-53285).
                // Set once the store tab has been opened so the modal does
                // not look frozen in its pre-redirect state when the user
                // returns from checkout.
                $modalScope.redirectSucceeded = false;

                // Remember which list item opened the panel so we can restore
                // focus to the matching button after Angular re-renders the
                // main content (the originally-clicked DOM node is destroyed
                // by `ng-if="!purchaseConfirmation"`).
                var _lastFocusedDomain = null;
                // Direct DOM reference to the originating element. Preferred
                // over the domain-attribute lookup because it disambiguates
                // duplicate `data-purchase-domain` values (e.g. the success
                // banner button and a list item for the same domain).
                var _lastFocusedElement = null;
                // Monotonic token used to ignore stale fetchDomainPurchaseUrl
                // responses after the user cancels the confirmation panel.
                var _activeRequestToken = 0;

                function _findRenderedButton(domain) {
                    if (!domain) {
                        return null;
                    }
                    var escaped = window.CSS && typeof window.CSS.escape === "function"
                        ? window.CSS.escape(domain)
                        : /^[A-Za-z0-9.\-]+$/.test(domain) ? domain : null;
                    if (!escaped) {
                        return null;
                    }
                    var sel = '[data-purchase-domain="' + escaped + '"]';
                    return $document[0].querySelector(sel);
                }

                function _findModalHeading() {
                    // Fallback target if the originating button can't be
                    // found (e.g. the list re-loaded). Prefers whichever
                    // heading is currently rendered.
                    var doc = $document[0];
                    return doc.querySelector("#domainSearchModalTitle") ||
                           doc.querySelector("#domainSearchModalNoResultsTitle") ||
                           doc.querySelector("#domainSearchModalPendingTitle") ||
                           doc.querySelector("#domainSearchModalUnavailableTitle");
                }

                var FOCUSABLE_SELECTOR =
                    "a[href], button:not([disabled]), input:not([disabled]), " +
                    "select:not([disabled]), textarea:not([disabled]), " +
                    "[tabindex]:not([tabindex=\"-1\"])";

                function _trapFocus($event, container) {
                    if ($event.key !== "Tab" || !container) {
                        return;
                    }
                    var focusable = Array.prototype.slice.call(
                        container.querySelectorAll(FOCUSABLE_SELECTOR)
                    );
                    if (!focusable.length) {
                        $event.preventDefault();
                        return;
                    }
                    var first = focusable[0];
                    var last  = focusable[focusable.length - 1];
                    var active = $document[0].activeElement;
                    if ($event.shiftKey && active === first) {
                        last.focus();
                        $event.preventDefault();
                    } else if (!$event.shiftKey && active === last) {
                        first.focus();
                        $event.preventDefault();
                    }
                }

                function _restoreLastFocus() {
                    var domain = _lastFocusedDomain;
                    var element = _lastFocusedElement;
                    _lastFocusedDomain = null;
                    _lastFocusedElement = null;
                    $timeout(function() {
                        var target = null;
                        // Prefer the exact element that opened the panel,
                        // but only if it is still in the document (it may
                        // have been removed by an intervening re-render).
                        if (element && $document[0].contains(element)) {
                            target = element;
                        }
                        if (!target) {
                            target = _findRenderedButton(domain) || _findModalHeading();
                        }
                        if (target && typeof target.focus === "function") {
                            target.focus();
                        }
                    });
                }

                // rec must be a domain recommendation object: { domain, price, ... }
                // $event is the click event; its currentTarget is used as the
                // preferred focus-restoration target when the panel closes.
                $modalScope.purchaseDomain = function(rec, $event) {
                    _lastFocusedDomain = rec && rec.domain ? rec.domain : null;
                    _lastFocusedElement = $event && $event.currentTarget ? $event.currentTarget : null;
                    $modalScope.purchaseConfirmation = rec;

                    // Track purchase intent at the moment the confirmation
                    // panel opens. Deferred via _storeConfigReady so
                    // _getMode() reports the correct value even when the user
                    // clicks before fetchStoreConfig() has settled.
                    _storeConfigReady.finally(function() {
                        _trackEvent("Domain-Purchase-Clicked", {
                            domain: rec && rec.domain ? rec.domain : null,
                            mode: _getMode(),
                            source: _source,
                        });
                    });

                    // Move focus to the new heading so screen readers
                    // announce the panel change. The heading carries
                    // tabindex="-1" specifically for this purpose.
                    $timeout(function() {
                        var heading = $document[0].querySelector("#domainSearchModalConfirmationTitle");
                        if (heading) {
                            heading.focus();
                        }
                    });
                };

                $modalScope.cancelPurchaseConfirmation = function() {
                    // Invalidate any in-flight fetchDomainPurchaseUrl call so
                    // its resolution cannot reopen the panel or call
                    // window.open() after the user has cancelled.
                    _activeRequestToken++;
                    $modalScope.purchasingDomain = null;
                    $modalScope.purchaseConfirmation = null;
                    $modalScope.redirectFailed = false;
                    $modalScope.redirectSucceeded = false;
                    $modalScope.blockedPurchaseUrl = null;
                    _restoreLastFocus();
                };

                // Stop Escape bubbling so ui-bootstrap's modal-level handler
                // does not also dismiss the whole modal. Tab/Shift+Tab are
                // cycled within the card to satisfy the WCAG 2.1 focus-trap
                // requirement for role="dialog".
                $modalScope.handleConfirmationKeydown = function($event) {
                    if ($event.key === "Escape") {
                        $modalScope.cancelPurchaseConfirmation();
                        $event.stopPropagation();
                        $event.preventDefault();
                    } else {
                        _trapFocus($event, $document[0].querySelector(
                            ".domain-search-modal__confirmation"
                        ));
                    }
                };

                // Surface the "Redirection Failed" overlay (Figma node
                // 86:10861). purchaseConfirmation is left set so the failure
                // panel can retry the redirect or the user can dismiss back
                // to the confirmation panel with Escape.
                function _showRedirectFailure() {
                    $modalScope.redirectFailed = true;
                    $timeout(function() {
                        var heading = $document[0].querySelector("#domainSearchModalFailureTitle");
                        if (heading) {
                            heading.focus();
                        }
                    });
                }

                // Swap the confirmation panel for the post-redirect success
                // state (CPANEL-53285, option A). purchaseConfirmation is
                // left set so the success panel keeps showing the purchased
                // domain.
                function _showRedirectSuccess() {
                    $modalScope.redirectSucceeded = true;
                    $timeout(function() {
                        var heading = $document[0].querySelector("#domainSearchModalSuccessTitle");
                        if (heading) {
                            heading.focus();
                        }
                    });
                }

                $modalScope.confirmPurchase = function() {
                    if (!$modalScope.purchaseConfirmation || $modalScope.purchasingDomain) {
                        return;
                    }
                    var domain = $modalScope.purchaseConfirmation.domain;
                    $modalScope.purchasingDomain = domain;
                    $modalScope.redirectFailed = false;
                    // Clear any stale popup-blocked banner from a previous
                    // attempt so a retry does not surface the old URL.
                    $modalScope.blockedPurchaseUrl = null;
                    // Capture the token for this request; if the user cancels
                    // before the promise settles, _activeRequestToken will be
                    // incremented and we will ignore the response below.
                    var token = ++_activeRequestToken;
                    $domainsService.fetchDomainPurchaseUrl(domain, 1)
                        .then(function(result) {
                            if (token !== _activeRequestToken) {
                                return;
                            }
                            var purchaseUrl = result && result.purchase_url;
                            if (purchaseUrl && /^https?:\/\//i.test(purchaseUrl)) {

                                // Fire the store-visited event BEFORE
                                // window.open() so it is unambiguously
                                // associated with the original page's network
                                // context. The browser context-switches when
                                // the new tab is created, which can otherwise
                                // drop the XHR from the originating page in
                                // browser tooling. Mode is accurate here
                                // because the fetchDomainPurchaseUrl round-trip
                                // settles well after fetchStoreConfig.
                                _trackEvent("Domain-Retail-Store-Visited", {
                                    source: _source,
                                    mode: _getMode(),
                                    domain: domain,
                                    store_url: purchaseUrl,
                                });
                                // Don't pass "noopener" in the features
                                // string: it makes window.open() return null
                                // even on success in some browsers, which
                                // would defeat the popup-blocked detection
                                // below. Sever the opener manually instead
                                // to keep the no-back-reference guarantee.
                                var openedWindow = window.open(purchaseUrl, "_blank");
                                if (openedWindow) {
                                    openedWindow.opener = null;
                                    $modalScope.redirectFailed = false;
                                    // _showRedirectSuccess() leaves
                                    // purchaseConfirmation set so the
                                    // post-redirect success panel
                                    // (CPANEL-53285) keeps showing the
                                    // purchased domain — don't clear it
                                    // here.
                                    _showRedirectSuccess();
                                } else {
                                    // Pop-up blocker fired: keep the
                                    // "Redirecting to Store" overlay open and
                                    // surface the URL via an inline banner
                                    // rendered inside the overlay, just above
                                    // the Go Back / Continue actions. Move
                                    // focus to the fallback link once the
                                    // banner renders so keyboard and
                                    // screen-reader users land on the
                                    // actionable element.
                                    $modalScope.blockedPurchaseUrl = purchaseUrl;
                                    $timeout(function() {
                                        // This id must stay in sync with the
                                        // fallback link's id in
                                        // domainSearchModal.ptt.
                                        var linkEl = $document[0].getElementById(
                                            "domainSearchPopupBlockedLink"
                                        );
                                        if (linkEl) {
                                            linkEl.focus();
                                        }
                                    });
                                }
                            } else {
                                _showRedirectFailure();
                            }
                        })
                        .catch(function() {
                            if (token !== _activeRequestToken) {
                                return;
                            }
                            _showRedirectFailure();
                        })
                        .finally(function() {
                            if (token === _activeRequestToken) {
                                $modalScope.purchasingDomain = null;
                            }
                        });
                };

                // "Continue" action on the "Redirection Failed" overlay —
                // dismiss both the failure overlay and the confirmation panel
                // and return the user to the search results. We deliberately do
                // NOT re-attempt the redirect here: retrying re-fires the same
                // failing fetchDomainPurchaseUrl call and traps the user in a
                // failure loop (CPANEL-53113). cancelPurchaseConfirmation()
                // already invalidates any in-flight request, clears the panel
                // state, and restores focus to the originating button.
                $modalScope.dismissRedirectFailure = function() {
                    $modalScope.cancelPurchaseConfirmation();
                };

                // Escape from the failure overlay returns to the confirmation
                // panel (rather than closing the whole modal) and restores
                // focus to its heading. Tab/Shift+Tab cycle within the card.
                $modalScope.handleFailureKeydown = function($event) {
                    if ($event.key === "Escape") {
                        $modalScope.redirectFailed = false;
                        $event.stopPropagation();
                        $event.preventDefault();
                        $timeout(function() {
                            var heading = $document[0].querySelector("#domainSearchModalConfirmationTitle");
                            if (heading) {
                                heading.focus();
                            }
                        });
                    } else {
                        _trapFocus($event, $document[0].querySelector(
                            ".domain-search-modal__confirmation"
                        ));
                    }
                };

                // "Done" on the post-redirect success panel — dismiss both
                // the success overlay and the surrounding search modal
                // (CPANEL-53285). The domain list is intentionally not
                // refreshed: there is no post-purchase automation yet, so a
                // reload would not surface the new domain.
                $modalScope.dismissRedirectSuccess = function() {
                    $modalScope.closeModal();
                };

                // "Add Domain" on the post-redirect success panel — hand
                // the just-purchased domain back to the host (via the
                // onAddDomain callback) so it can open its add-domain flow
                // with the name prefilled, then dismiss this modal. Only
                // rendered when a callback was supplied (see canAddDomain).
                // The domain comes from
                // purchaseConfirmation, which _showRedirectSuccess()
                // deliberately leaves set on the success panel.
                $modalScope.addPurchasedDomain = function() {
                    var domain = $modalScope.purchaseConfirmation &&
                        $modalScope.purchaseConfirmation.domain;
                    if (!_onAddDomain || !domain) {
                        return;
                    }
                    _trackEvent("Domain-Add-After-Purchase-Clicked", {
                        source: _source,
                        mode: _getMode(),
                        domain: domain,
                    });
                    $modalScope.closeModal();
                    _onAddDomain(domain);
                };

                // Escape from the success overlay mirrors its ✕ action and
                // closes the whole modal. Returning to the search results is
                // a deliberate, explicit choice via the panel's "Go Back"
                // link (cancelPurchaseConfirmation); ✕, Escape, and backdrop
                // are the "I'm done" dismiss gesture and are kept distinct
                // from it. Tab/Shift+Tab cycle within the card.
                $modalScope.handleSuccessKeydown = function($event) {
                    if ($event.key === "Escape") {
                        $modalScope.dismissRedirectSuccess();
                        $event.stopPropagation();
                        $event.preventDefault();
                    } else {
                        _trapFocus($event, $document[0].querySelector(
                            ".domain-search-modal__confirmation"
                        ));
                    }
                };

                $modalScope.closeModal = function() {
                    $uibModalInstance.dismiss("cancel");
                };

                if (options.autoSearch && $modalScope.searchQuery) {

                    // searchDomains() defers its Mixpanel call via
                    // _storeConfigReady internally, so mode is accurate even
                    // when the modal opens before fetchStoreConfig settles.
                    $modalScope.searchDomains();
                }
            },
        ];

        /**
         * Service that opens the reusable Domain Search modal.
         *
         * @example
         * domainSearchModalService.open({
         *     cachedExploreData: { featured: [...], available: [...] },
         *     initialQuery: "catclothes",
         *     initialTldFilters: [".com"],
         *     autoSearch: true,
         * });
         *
         * @return {Object} The $uibModal instance.
         */
        module.factory("domainSearchModalService", ["$uibModal", "$document",
            function($uibModal, $document) {
                return {
                    TEMPLATE_URL: TEMPLATE_URL,
                    WINDOW_CLASS: WINDOW_CLASS,
                    ModalController: ModalController,
                    open: function(options) {
                        var modalInstance = $uibModal.open({
                            templateUrl: TEMPLATE_URL,
                            windowClass: WINDOW_CLASS,
                            controller: ModalController,
                            resolve: {
                                options: function() {
                                    return options || {};
                                },
                            },
                        });
                        modalInstance.rendered.then(function() {
                            var dialogEl = $document[0].querySelector(
                                "." + WINDOW_CLASS + " [role='dialog']"
                            );
                            if (dialogEl) {
                                dialogEl.setAttribute(
                                    "aria-labelledby",
                                    "domainSearchModalTitle domainSearchModalNoResultsTitle domainSearchModalPendingTitle domainSearchModalUnavailableTitle domainSearchModalConfirmationTitle domainSearchModalSuccessTitle domainSearchModalFailureTitle"
                                );
                            }
                        });
                        return modalInstance;
                    },
                };
            },
        ]);

        return {
            namespace: MODULE_NAMESPACE,
            ModalController: ModalController,
        };
    }
);
Back to Directory