PageSourceSearch

https://www.cafepress.ca/scripts/customer.js?30ee96d925107cd1f2b86141b0ad71ffab79167c

js cafepress.ca collected 2026-09-25 14:18:06 UTC 12,918 bytes, 325 lines download raw bytes

1/**
2 * PACustomer — turns the guest header rendered by the server into the
3 * signed-in header on the client.
4 *
5 * Loaded globally (bundled via script_footer.php) in the order api.js ->
6 * section.js -> mini-cart.js -> customer.js -> init.js.
7 *
8 * Pages are shared by every visitor through the Full Page Cache, so templates
9 * always render the guest variant and mark the pieces that differ once we know
10 * who the visitor is. Three declarative hooks cover the whole header:
11 *
12 *   data-auth="guest" / data-auth="member"
13 *     Shown only to that audience. Guest nodes are visible in the cached HTML
14 *     and hidden once a signed-in customer is known; member nodes are the
15 *     reverse, and therefore must be rendered with the `hide` class so they
16 *     never flash for guests. Visibility is toggled via the `hide` class, which
17 *     is the site-wide convention.
18 *
19 *   data-auth-label
20 *     Optional text slot (PCR account chrome). Stays empty until the customer
21 *     section is known so "Login / Register" does not paint and then jump to
22 *     "Account". data-label-guest / data-label-member supply the copy.
23 *
24 *   data-customer-firstname
25 *     textContent is replaced with the customer's first name (or email when the
26 *     account has no name). Only meaningful inside a data-auth="member" node.
27 *
28 *   data-init-profile-menu
29 *     The account dropdown. FPC pages ship the guest menu (identical for every
30 *     visitor on that URL). Uncached pages set data-menu-audience to member when
31 *     the session is signed in, so Account Settings does not flash Sign In.
32 *     A signed-in visitor on an FPC page fetches GET /rest/web/mine/profile-menu
33 *     once the customer section says who they are, or on hover if the on-page
34 *     menu is still the guest variant. While that fetch is outstanding the
35 *     dropdown stays closed (data-menu-pending) so Sign In cannot flash.
36 *     Uncached pages that already shipped the member menu are never held.
37 *
38 * The customer section is hydrated by SectionStore ('customer' section, see
39 * SectionController::loadCustomerData). SectionStore returns the cached payload
40 * synchronously and delivers fresh data through onUpdated, so a returning
41 * visitor sees the signed-in header without waiting for the network.
42 */
43window.PACustomer = (function () {
44    var AUTH_ATTR = 'data-auth';
45    var LABEL_ATTR = 'data-auth-label';
46    var NAME_ATTR = 'data-customer-firstname';
47    var MENU_ATTR = 'data-init-profile-menu';
48    var HIDE_CLASS = 'hide';
49    var ENDPOINT = '/rest/web/mine/profile-menu';
50
51    // { logged_in, guest, id, email, firstname } from SectionStore('customer').
52    var cache = null;
53    // Who the menu currently on the page is for. FPC pages start as guest;
54    // uncached /secure pages may already ship the member menu (data-menu-audience).
55    var menuAudience = 'guest';
56    var fetchInFlight = null;
57    // False until SectionStore delivers the first customer payload this page load.
58    var customerSectionKnown = false;
59    // True while the pointer is over the account icon; used to open the menu
60    // after a pending fetch without flashing the guest Sign In links first.
61    var pointerOverProfile = false;
62
63    function readInitialMenuAudience() {
64        var node = document.querySelector('[' + MENU_ATTR + ']');
65        if (!node) return 'guest';
66        return node.getAttribute('data-menu-audience') === 'member' ? 'member' : 'guest';
67    }
68
69    /**
70     * PHPSESSID is HttpOnly and invisible to JavaScript, so it cannot be used
71     * as a live-session check. Trust the customer section (localStorage until
72     * the first sync, then the server). Logout clears localStorage; a stale
73     * "logged in" after the session dies is corrected when the sync lands.
74     */
75    function isLoggedIn() {
76        return !!(cache && cache.logged_in);
77    }
78
79    function audienceForVisitor() {
80        return isLoggedIn() ? 'member' : 'guest';
81    }
82
83    function toggle(el, visible) {
84        if (visible) el.classList.remove(HIDE_CLASS);
85        else el.classList.add(HIDE_CLASS);
86    }
87
88    function renderAudience(scope) {
89        var loggedIn = isLoggedIn();
90        var nodes = scope.querySelectorAll('[' + AUTH_ATTR + ']');
91        Array.prototype.forEach.call(nodes, function (el) {
92            var audience = el.getAttribute(AUTH_ATTR);
93            if (audience === 'member') toggle(el, loggedIn);
94            else if (audience === 'guest') toggle(el, !loggedIn);
95        });
96    }
97
98    /**
99     * Status.user_id / Status.cart_id used to be printed into the page footer.
100     * They identify the visitor, so shared cached HTML cannot carry them; the
101     * image-retry diagnostics in functions.js read them from Status, which is
102     * populated here instead. cart_id comes straight from its cookie (the name
103     * is site-scoped, so the server still supplies it as CART_ID_COOKIE).
104     */
105    function publishStatus() {
106        if (typeof Status === 'undefined' || !Status.set) return;
107        Status.set('user_id', (isLoggedIn() && cache && cache.token) || 0);
108        if (typeof CART_ID_COOKIE !== 'undefined' && typeof readCookie === 'function') {
109            Status.set('cart_id', readCookie(CART_ID_COOKIE) || '');
110        }
111    }
112
113    /**
114     * gtm.php emits the identity keys in their guest form because the page is
115     * shared by the Full Page Cache; the real values are pushed here. `events`
116     * carries the one-shot login / sign_up events the section consumed on the
117     * server, so each is pushed exactly once per payload.
118     */
119    function pushGtm(data) {
120        if (!data || typeof window.dataLayer === 'undefined') return;
121        if (data.gtm) window.dataLayer.push(data.gtm);
122
123        var events = data.events || [];
124        if (!events.length) return;
125        // The section is persisted in localStorage and relayed to other tabs, so
126        // clear the one-shot events before pushing them or they fire again there.
127        data.events = [];
128        if (typeof window.SectionStore !== 'undefined') {
129            window.SectionStore.setLocal('customer', data);
130        }
131        events.forEach(function (event) {
132            window.dataLayer.push(event);
133        });
134    }
135
136    function renderName(scope) {
137        var name = (isLoggedIn() && cache && (cache.firstname || cache.email)) || '';
138        var nodes = scope.querySelectorAll('[' + NAME_ATTR + ']');
139        Array.prototype.forEach.call(nodes, function (el) {
140            el.textContent = name;
141        });
142    }
143
144    function isCustomerKnown() {
145        return customerSectionKnown || cache !== null;
146    }
147
148    function renderAuthLabels(scope) {
149        var known = isCustomerKnown();
150        var loggedIn = isLoggedIn();
151        var nodes = scope.querySelectorAll('[' + LABEL_ATTR + ']');
152        Array.prototype.forEach.call(nodes, function (el) {
153            if (!known) {
154                el.textContent = '';
155                return;
156            }
157            var guest = el.getAttribute('data-label-guest') || '';
158            var member = el.getAttribute('data-label-member') || '';
159            el.textContent = loggedIn ? member : guest;
160        });
161    }
162
163    function render(scope) {
164        var root = scope || document;
165        renderAudience(root);
166        renderAuthLabels(root);
167        renderName(root);
168        publishStatus();
169    }
170
171    function eachMenu(fn) {
172        Array.prototype.forEach.call(document.querySelectorAll('[' + MENU_ATTR + ']'), fn);
173    }
174
175    function injectMenu(html) {
176        eachMenu(function (n) { n.innerHTML = html; });
177    }
178
179    function menuFitsVisitor() {
180        return menuAudience === audienceForVisitor();
181    }
182
183    /**
184     * Hold the dropdown closed only while FPC guest markup would flash Sign In.
185     * Uncached /secure pages ship the member menu; do not treat a missing or
186     * guest-shaped customer cache as pending — needsMenuFetch already skips
187     * replacing that markup until the section answers.
188     */
189    function shouldHoldMenuClosed() {
190        if (menuAudience === 'member') return false;
191        return isLoggedIn() || !!fetchInFlight;
192    }
193
194    /**
195     * top_menu.php shows the dropdown on hover. Mark the node pending so that
196     * handler does not reveal Sign In while we still owe the signed-in markup.
197     */
198    function syncMenuPending() {
199        var pending = shouldHoldMenuClosed();
200        eachMenu(function (n) {
201            if (pending) n.setAttribute('data-menu-pending', '1');
202            else n.removeAttribute('data-menu-pending');
203        });
204    }
205
206    function hideMenus() {
207        eachMenu(function (n) { n.classList.add(HIDE_CLASS); });
208    }
209
210    function revealMenuIfHovering() {
211        if (!pointerOverProfile || shouldHoldMenuClosed()) return;
212        eachMenu(function (n) { n.classList.remove(HIDE_CLASS); });
213    }
214
215    function fetchMenu() {
216        if (fetchInFlight) return fetchInFlight;
217        if (typeof window.PAApi === 'undefined') return Promise.resolve(null);
218        fetchInFlight = window.PAApi.request(ENDPOINT, {
219            query: { path: window.location.pathname },
220        }).then(function (res) {
221            if (res && typeof res.html === 'string') {
222                injectMenu(res.html);
223                // The server renders from the live session; trust that over the
224                // client cache when the fetch started before SectionStore answered.
225                menuAudience = res.signed_in ? 'member' : 'guest';
226            }
227            return res;
228        }).catch(function () {
229            return null;
230        }).then(function (res) {
231            fetchInFlight = null;
232            syncMenuPending();
233            revealMenuIfHovering();
234            return res;
235        });
236        syncMenuPending();
237        return fetchInFlight;
238    }
239
240    /**
241     * Fetch when the on-page menu is for the wrong audience. Do not fetch while
242     * an uncached page already shipped the member menu and the section has not
243     * answered yet — that would race and can replace Sign Out with Sign In.
244     */
245    function needsMenuFetch() {
246        if (fetchInFlight) return false;
247        if (!customerSectionKnown && menuAudience === 'member') return false;
248        return !menuFitsVisitor();
249    }
250
251    function onHover() {
252        pointerOverProfile = true;
253        if (needsMenuFetch()) fetchMenu();
254    }
255
256    function onLeave() {
257        pointerOverProfile = false;
258    }
259
260    /**
261     * Replace the menu when it is for the wrong audience. Hide guest Sign In
262     * links rather than leave them on screen until the member HTML lands.
263     * Member markup already on the page stays hoverable.
264     */
265    function refreshMenuIfStale() {
266        syncMenuPending();
267        if (shouldHoldMenuClosed()) hideMenus();
268        if (needsMenuFetch()) fetchMenu();
269        else revealMenuIfHovering();
270    }
271
272    function onUpdatedCustomer(data) {
273        customerSectionKnown = true;
274        var wasLoggedIn = isLoggedIn();
275        cache = data || null;
276        // Guest favorites live under the cart cookie version key; after sign-in
277        // the favorite section is keyed by customer id. Drop the stale guest
278        // localStorage payload so init.js re-renders hearts and the badge from
279        // the server instead of keeping count=0 / design_ids=[] until CDC catches up.
280        if (!wasLoggedIn && isLoggedIn() && typeof window.SectionStore !== 'undefined') {
281            window.SectionStore.invalidate('favorite');
282            window.SectionStore.invalidate('cart');
283        }
284        render(document);
285        pushGtm(cache);
286        refreshMenuIfStale();
287    }
288
289    function prime() {
290        menuAudience = readInitialMenuAudience();
291        if (typeof window.SectionStore === 'undefined') return;
292        var cached = window.SectionStore.get('customer');
293        if (cached) cache = cached;
294        window.SectionStore.onUpdated('customer', onUpdatedCustomer);
295        refreshMenuIfStale();
296    }
297
298    function init() {
299        prime();
300        // Runs even without a cached section: it re-asserts the guest state the
301        // HTML already shows and publishes the cart id, which comes from a cookie.
302        var ready = function () { render(document); };
303        if (typeof jQuery !== 'undefined') {
304            jQuery(function () { ready(); });
305        } else if (document.readyState === 'loading') {
306            document.addEventListener('DOMContentLoaded', ready);
307        } else {
308            ready();
309        }
310        var hoverTargets = document.querySelectorAll('[e-show-mini-profile]');
311        Array.prototype.forEach.call(hoverTargets, function (el) {
312            el.addEventListener('mouseenter', onHover);
313            el.addEventListener('mouseleave', onLeave);
314        });
315    }
316
317    init();
318
319    return {
320        render: render,
321        fetchMenu: fetchMenu,
322        isLoggedIn: isLoggedIn,
323        getCachedCustomer: function () { return cache; },
324    };
325})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.