PageSourceSearch

https://guidasphere.com/assets/runtime.bridge.js?v=1788888326831

js guidasphere.com collected 2026-10-02 01:31:09 UTC 70,908 bytes, 1,643 lines download raw bytes

1(() => {
2  // Strict auth storage keys — no guessing, no fallbacks.
3  const AUTH_TOKEN_KEY = 'access_token';
4  const AUTH_REFRESH_KEY = 'refresh_token';
5  const AUTH_USER_KEY = 'auth_user';
6  const AUTH_BACKUP_KEY = 'guidasphere.auth.navigation_backup';
7  const protectedPrefixes = ['/account', '/seller', '/admin', '/hr', '/support', '/finance', '/moderation', '/referrals', '/onboarding-assist-agent'];
8        const roleBridgeId = 'guidasphere-role-bridge-nav';
9        const redirectNoticeKey = 'guidasphere.runtime.redirect.notice';
10        const apiBaseFromStorage = "";
11        const apiBase = (window.GUIDASPHERE_API_BASE_URL || apiBaseFromStorage || '').replace(/\/+$/, '');
12        const defaultProductImage = '/project-assets/products/default-product.svg';
13        const runtimeHelpers = window.__GUIDASPHERE_RUNTIME_HELPERS__ || {};
14        const runtimeRoleBridge = window.__GUIDASPHERE_RUNTIME_ROLE_BRIDGE__ || {};
15        const runtimeAuth = window.__GUIDASPHERE_RUNTIME_AUTH__ || {};
16        const runtimeNavigation = window.__GUIDASPHERE_RUNTIME_NAV__ || {};
17        const runtimeApis = window.__GUIDASPHERE_RUNTIME_APIS__ || {};
18        const runtimeCatalogPages = window.__GUIDASPHERE_RUNTIME_CATALOG_PAGES__ || {};
19        const runtimeOnboarding = window.__GUIDASPHERE_RUNTIME_ONBOARDING__ || {};
20        const runtimeAccount = window.__GUIDASPHERE_RUNTIME_ACCOUNT__ || {};
21        const runtimeAuthFlows = window.__GUIDASPHERE_RUNTIME_AUTH_FLOWS__ || {};
22        const runtimeLifecycle = window.__GUIDASPHERE_RUNTIME_LIFECYCLE__ || {};
23        const nativeAlert = typeof window.alert === 'function' ? window.alert.bind(window) : null;
24        let lifecycleController = null;
25        let catalogPagesController = null;
26        function getRuntimeAlertController() {
27          if (window.__GUIDASPHERE_RUNTIME_ALERT_CONTROLLER__) {
28            return window.__GUIDASPHERE_RUNTIME_ALERT_CONTROLLER__;
29          }
30
31          const overlay = document.createElement('div');
32          overlay.id = 'guidasphere-runtime-alert-modal';
33          overlay.style.position = 'fixed';
34          overlay.style.inset = '0';
35          overlay.style.background = 'rgba(0, 0, 0, 0.5)';
36          overlay.style.display = 'none';
37          overlay.style.alignItems = 'center';
38          overlay.style.justifyContent = 'center';
39          overlay.style.padding = '16px';
40          overlay.style.zIndex = '2147483647';
41
42          const panel = document.createElement('div');
43          panel.style.width = '100%';
44          panel.style.maxWidth = '440px';
45          panel.style.background = '#ffffff';
46          panel.style.borderRadius = '12px';
47          panel.style.boxShadow = '0 20px 45px rgba(0, 0, 0, 0.28)';
48          panel.style.border = '1px solid rgba(17, 24, 39, 0.08)';
49          panel.style.padding = '18px';
50
51          const title = document.createElement('h3');
52          title.textContent = 'Notice';
53          title.style.margin = '0';
54          title.style.fontSize = '18px';
55          title.style.fontWeight = '700';
56          title.style.color = '#111827';
57
58          const message = document.createElement('p');
59          message.style.margin = '10px 0 0';
60          message.style.fontSize = '14px';
61          message.style.lineHeight = '1.45';
62          message.style.color = '#374151';
63
64          const actions = document.createElement('div');
65          actions.style.display = 'flex';
66          actions.style.justifyContent = 'flex-end';
67          actions.style.marginTop = '14px';
68
69          const closeButton = document.createElement('button');
70          closeButton.type = 'button';
71          closeButton.textContent = 'Close';
72          closeButton.style.border = 'none';
73          closeButton.style.cursor = 'pointer';
74          closeButton.style.borderRadius = '8px';
75          closeButton.style.padding = '8px 14px';
76          closeButton.style.fontSize = '14px';
77          closeButton.style.fontWeight = '600';
78          closeButton.style.background = '#f97316';
79          closeButton.style.color = '#ffffff';
80
81          actions.appendChild(closeButton);
82          panel.appendChild(title);
83          panel.appendChild(message);
84          panel.appendChild(actions);
85          overlay.appendChild(panel);
86          document.body.appendChild(overlay);
87
88          let onDismiss = null;
89          const hide = () => {
90            overlay.style.display = 'none';
91            document.body.style.removeProperty('overflow');
92            if (typeof onDismiss === 'function') {
93              try {
94                onDismiss();
95              } catch (_) {}
96            }
97            onDismiss = null;
98          };
99
100          closeButton.addEventListener('click', () => hide());
101          overlay.addEventListener('click', (event) => {
102            if (event.target === overlay) hide();
103          });
104          document.addEventListener('keydown', (event) => {
105            if (event.key === 'Escape' && overlay.style.display !== 'none') {
106              hide();
107            }
108          });
109
110          const controller = {
111            show(params = {}) {
112              const nextTitle = String(params.title || 'Notice').trim() || 'Notice';
113              const nextMessage = String(params.message || '').trim() || 'Something went wrong.';
114              onDismiss = typeof params.onDismiss === 'function' ? params.onDismiss : null;
115              title.textContent = nextTitle;
116              message.textContent = nextMessage;
117              overlay.style.display = 'flex';
118              document.body.style.overflow = 'hidden';
119              closeButton.focus();
120            },
121            hide,
122          };
123
124          window.__GUIDASPHERE_RUNTIME_ALERT_CONTROLLER__ = controller;
125          return controller;
126        }
127
128        function showRuntimeAlert(message, title = 'Notice') {
129          try {
130            const controller = getRuntimeAlertController();
131            controller.show({ title, message });
132            return;
133          } catch (_) {}
134          if (nativeAlert) {
135            nativeAlert(String(message || 'Something went wrong.'));
136          }
137        }
138
139        function installRuntimeAlertPatch() {
140          if (window.__GUIDASPHERE_RUNTIME_ALERT_PATCHED__) return;
141          window.__GUIDASPHERE_RUNTIME_ALERT_PATCHED__ = true;
142          window.__GUIDASPHERE_NATIVE_ALERT__ = nativeAlert;
143          window.alert = (message) => {
144            showRuntimeAlert(message, 'Notice');
145          };
146          
147          function createOverlay() {
148            const overlay = document.createElement('div');
149            overlay.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-40 backdrop-blur-sm transition-opacity duration-200 opacity-0 p-4 sm:p-6';
150            requestAnimationFrame(() => {
151                overlay.classList.remove('opacity-0');
152                overlay.classList.add('opacity-100');
153            });
154            return overlay;
155          }
156
157          function createModalBox(titleText, messageText) {
158            const box = document.createElement('div');
159            box.className = 'relative max-h-full w-full max-w-sm overflow-y-auto bg-white overflow-hidden transform scale-95 transition-transform duration-200';
160            box.style.borderRadius = "14px";
161            box.style.boxShadow = "0 22px 45px rgba(15, 23, 42, 0.3)";
162            requestAnimationFrame(() => {
163                box.classList.remove('scale-95');
164                box.classList.add('scale-100');
165            });
166            const header = document.createElement('div');
167            header.className = 'px-6 py-4 border-b border-gray-100';
168            const title = document.createElement('h3');
169            title.className = 'text-lg font-semibold text-gray-900';
170            title.textContent = titleText;
171            const body = document.createElement('div');
172            body.className = 'px-6 py-4';
173            const message = document.createElement('p');
174            message.className = 'text-sm text-gray-600';
175            message.textContent = messageText;
176            header.appendChild(title);
177            body.appendChild(message);
178            box.appendChild(header);
179            box.appendChild(body);
180            return box;
181          }
182
183          function createButton(text, primary = true, color = 'orange') {
184            const btn = document.createElement('button');
185            btn.textContent = text;
186            if (primary) {
187                btn.className = `px-4 py-2 bg-${color}-600 text-white text-sm font-medium rounded-md hover:bg-${color}-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-${color}-500 transition-colors`;
188            } else {
189                btn.className = `px-4 py-2 bg-white text-gray-700 text-sm font-medium rounded-md border border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-${color}-500 transition-colors`;
190            }
191            return btn;
192          }
193
194          function closeOverlay(overlay) {
195            overlay.classList.remove('opacity-100');
196            overlay.classList.add('opacity-0');
197            const box = overlay.querySelector('.scale-100');
198            if (box) {
199                box.classList.remove('scale-100');
200                box.classList.add('scale-95');
201            }
202            setTimeout(() => {
203                if (overlay.parentNode) {
204                    overlay.parentNode.removeChild(overlay);
205                }
206            }, 200);
207          }
208
209          window.customAlert = function(message, title = "Alert") {
210            return new Promise((resolve) => {
211                const overlay = createOverlay();
212                const box = createModalBox(title, message);
213                const footer = document.createElement('div');
214                footer.className = 'px-6 py-4 bg-gray-50 flex justify-end';
215                const okBtn = createButton('OK', true, 'orange');
216                okBtn.addEventListener('click', () => {
217                    closeOverlay(overlay);
218                    resolve();
219                });
220                footer.appendChild(okBtn);
221                box.appendChild(footer);
222                overlay.appendChild(box);
223                document.body.appendChild(overlay);
224                okBtn.focus();
225            });
226          };
227
228          window.customConfirm = function(message, title = "Confirm") {
229            return new Promise((resolve) => {
230                const overlay = createOverlay();
231                const box = createModalBox(title, message);
232                const footer = document.createElement('div');
233                footer.className = 'px-6 py-4 bg-gray-50 flex justify-end space-x-3';
234                const cancelBtn = createButton('Cancel', false, 'gray');
235                const okBtn = createButton('Delete', true, 'red');
236                cancelBtn.addEventListener('click', () => {
237                    closeOverlay(overlay);
238                    resolve(false);
239                });
240                okBtn.addEventListener('click', () => {
241                    closeOverlay(overlay);
242                    resolve(true);
243                });
244                footer.appendChild(cancelBtn);
245                footer.appendChild(okBtn);
246                box.appendChild(footer);
247                overlay.appendChild(box);
248                document.body.appendChild(overlay);
249                okBtn.focus();
250            });
251          };
252        }
253
254        function persistRedirectNotice(message, title) {
255          const text = String(message || '').trim();
256          if (!text) return;
257          try {
258            sessionStorage.setItem(
259              redirectNoticeKey,
260              JSON.stringify({
261                message: text,
262                title: String(title || 'Access Restricted').trim() || 'Access Restricted',
263              }),
264            );
265          } catch (_) {}
266        }
267
268        function showPendingRedirectNotice() {
269          let raw = '';
270          try {
271            raw = String(sessionStorage.getItem(redirectNoticeKey) || '');
272            sessionStorage.removeItem(redirectNoticeKey);
273          } catch (_) {
274            raw = '';
275          }
276          if (!raw) return;
277          try {
278            const payload = JSON.parse(raw);
279            const message = String(payload?.message || '').trim();
280            const title = String(payload?.title || 'Access Restricted').trim() || 'Access Restricted';
281            if (!message) return;
282            window.setTimeout(() => {
283              showRuntimeAlert(message, title);
284            }, 0);
285          } catch (_) {}
286        }
287        installRuntimeAlertPatch();
288        showPendingRedirectNotice();
289
290        const runtimeApiBundle = typeof runtimeApis.createRuntimeApis === 'function'
291          ? runtimeApis.createRuntimeApis({
292            apiBase,
293            defaultProductImage,
294            fetchImpl: fetch,
295          })
296          : null;
297
298        const catalogApi = runtimeApiBundle && runtimeApiBundle.catalogApi
299          ? runtimeApiBundle.catalogApi
300          : {
301            async listProducts() { return []; },
302            async getProduct() { return null; },
303            async getProductDetail() { return null; },
304            async listCategories() { return []; },
305            async listCategoryProducts() { return []; },
306            async listBrands() { return []; },
307            async getBrand() { return null; },
308            async listBrandProducts() { return []; },
309          };
310
311        const contentApi = runtimeApiBundle && runtimeApiBundle.contentApi
312          ? runtimeApiBundle.contentApi
313          : {
314            async listStores() { return []; },
315            async getStore() { return null; },
316            async listStoresMerged(fallbackStores = []) { return Array.isArray(fallbackStores) ? fallbackStores : []; },
317            async getStoreMerged(_storeId, fallbackStore = null) { return fallbackStore; },
318            async listBlogPosts() { return []; },
319            async getBlogPost() { return null; },
320            async listBlogPostsMerged(fallbackPosts = []) { return Array.isArray(fallbackPosts) ? fallbackPosts : []; },
321            async getBlogPostMerged(_postId, fallbackPost = null) { return fallbackPost; },
322            async listCareers() { return []; },
323            async getCareer() { return null; },
324            async submitCareerApplication() { throw new Error('Please sign in or sign up to apply for this position.'); },
325            async listCareersMerged(fallbackCareers = []) { return Array.isArray(fallbackCareers) ? fallbackCareers : []; },
326            async getCareerMerged(_careerId, fallbackCareer = null) { return fallbackCareer; },
327          };
328
329        const cartApi = runtimeApiBundle && runtimeApiBundle.cartApi
330          ? runtimeApiBundle.cartApi
331          : {
332            async getCart() { return { cartId: 'local-cart', items: [], count: 0, totalItems: 0 }; },
333            async listItems() { return []; },
334            async addItem() { return null; },
335            async updateItem() { return null; },
336            async removeItem() {},
337            async clear() {},
338          };
339
340        const orderApi = runtimeApiBundle && runtimeApiBundle.orderApi
341          ? runtimeApiBundle.orderApi
342          : {
343            async listOrders() { return []; },
344            async trackOrder() { return null; },
345            async placeOrder() { return null; },
346          };
347
348        const paymentApi = runtimeApiBundle && runtimeApiBundle.paymentApi
349          ? runtimeApiBundle.paymentApi
350          : {
351            async verifyPaystackReference() {
352              throw new Error('Payment API is not available.');
353            },
354          };
355
356        window.GUIDASPHERE_CATALOG_API = catalogApi;
357        window.GUIDASPHERE_CONTENT_API = contentApi;
358        window.GUIDASPHERE_CART_API = cartApi;
359        window.GUIDASPHERE_ORDER_API = orderApi;
360        window.GUIDASPHERE_PAYMENT_API = paymentApi;
361        window.GUIDASPHERE_HR_API = runtimeApiBundle && runtimeApiBundle.hrApi ? runtimeApiBundle.hrApi : null;
362
363        catalogPagesController = typeof runtimeCatalogPages.createRuntimeCatalogPages === 'function'
364          ? runtimeCatalogPages.createRuntimeCatalogPages({
365            windowLike: window,
366            documentLike: document,
367            catalogApi,
368          })
369          : null;
370
371
372        function getAccessToken() {
373          if (typeof runtimeHelpers.getAccessToken === 'function') {
374            return runtimeHelpers.getAccessToken(localStorage, sessionStorage);
375          }
376          return localStorage.getItem(AUTH_TOKEN_KEY) || sessionStorage.getItem(AUTH_TOKEN_KEY) || null;
377        }
378
379        const accountRuntimeBundle = typeof runtimeAccount.createRuntimeAccountApi === 'function'
380          ? runtimeAccount.createRuntimeAccountApi({
381            apiBase,
382            fetchImpl: fetch,
383            windowLike: window,
384            localStore: localStorage,
385            sessionStore: sessionStorage,
386            localStore: localStorage,
387            sessionStore: sessionStorage,
388            getAccessToken,
389          })
390          : null;
391
392        const fallbackAccountSnapshot = {
393          user: {},
394          orders: [],
395          wishlist: [],
396          addresses: [],
397        };
398
399        const accountApi = accountRuntimeBundle && accountRuntimeBundle.api
400          ? accountRuntimeBundle.api
401          : {
402            getSnapshot() {
403              return {
404                user: { ...fallbackAccountSnapshot.user },
405                orders: [...fallbackAccountSnapshot.orders],
406                wishlist: [...fallbackAccountSnapshot.wishlist],
407                addresses: [...fallbackAccountSnapshot.addresses],
408              };
409            },
410            async refresh() {
411              return this.getSnapshot();
412            },
413            async listOrders() {
414              return [];
415            },
416            async hydrateOverviewPanel(options = {}) {
417              const fallbackUser = options && options.fallbackUser && typeof options.fallbackUser === 'object' ? options.fallbackUser : {};
418              const fallbackOrders = Array.isArray(options?.fallbackOrders) ? options.fallbackOrders : [];
419              if (typeof options?.setAccountUser === 'function') options.setAccountUser(fallbackUser);
420              if (typeof options?.setAccountOrders === 'function') options.setAccountOrders(fallbackOrders);
421              return { user: fallbackUser, orders: fallbackOrders };
422            },
423            async hydrateOrdersPanel(options = {}) {
424              const fallbackOrders = Array.isArray(options?.fallbackOrders) ? options.fallbackOrders : [];
425              if (typeof options?.setOrders === 'function') options.setOrders(fallbackOrders);
426              return fallbackOrders;
427            },
428            filterOrdersPanel(options = {}) {
429              const rows = Array.isArray(options?.orders) ? options.orders : [];
430              const nextStatus = String(options?.statusFilter || 'all').toLowerCase();
431              if (nextStatus === 'all') return rows;
432              return rows.filter((row) => String(row?.status || '').toLowerCase() === nextStatus);
433            },
434            countOrdersByStatus(options = {}) {
435              const rows = Array.isArray(options?.orders) ? options.orders : [];
436              const nextStatus = String(options?.status || 'all').toLowerCase();
437              if (nextStatus === 'all') return rows.length;
438              return rows.filter((row) => String(row?.status || '').toLowerCase() === nextStatus).length;
439            },
440            getOrderStatusClassName(status) {
441              switch (String(status || '').toLowerCase()) {
442                case 'delivered':
443                  return 'bg-green-100 text-green-700';
444                case 'shipped':
445                  return 'bg-blue-100 text-blue-700';
446                case 'pending':
447                  return 'bg-orange-100 text-orange-700';
448                case 'cancelled':
449                  return 'bg-red-100 text-red-700';
450                default:
451                  return 'bg-gray-100 text-gray-700';
452              }
453            },
454            formatCurrency(value, currencyCode) {
455              const amount = Number(value || 0);
456              const normalizedCurrency = String(currencyCode || '').trim().toUpperCase() || 'GHS';
457              try {
458                return new Intl.NumberFormat('en-US', {
459                  style: 'currency',
460                  currency: normalizedCurrency,
461                  minimumFractionDigits: 2,
462                  maximumFractionDigits: 2,
463                }).format(Number.isFinite(amount) ? amount : 0);
464              } catch (_) {
465                return `${normalizedCurrency} ${(Number.isFinite(amount) ? amount : 0).toFixed(2)}`;
466              }
467            },
468            formatMemberSinceLabel(value, options = {}) {
469              const token = String(value || '').trim();
470              if (!token) return String(options?.fallback || '--');
471              const parsed = new Date(token);
472              if (Number.isNaN(parsed.getTime())) return String(options?.fallback || '--');
473              return parsed.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
474            },
475            formatOrderDateLabel(value, options = {}) {
476              const token = String(value || '').trim();
477              if (!token) return String(options?.fallback || '--');
478              const parsed = new Date(token);
479              if (Number.isNaN(parsed.getTime())) return String(options?.fallback || '--');
480              return parsed.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
481            },
482            formatOrderPlacedLabel(value) {
483              const token = String(value || '').trim();
484              if (!token) return '--';
485              const parsed = new Date(token);
486              if (Number.isNaN(parsed.getTime())) return '--';
487              return parsed.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
488            },
489            buildRecentOrdersPanel(options = {}) {
490              const rows = Array.isArray(options?.orders) ? options.orders : [];
491              const limit = Number.isFinite(Number(options?.limit)) ? Number(options.limit) : 3;
492              return rows.slice(0, Math.max(0, limit));
493            },
494          };
495        window.GUIDASPHERE_ACCOUNT_API = accountApi;
496        if (accountRuntimeBundle && typeof accountRuntimeBundle.initialize === 'function') {
497          void accountRuntimeBundle.initialize().catch(() => {});
498        }
499
500        async function callLogout(token) {
501          if (typeof runtimeAuth.callLogout === 'function') {
502            await runtimeAuth.callLogout({
503              apiBase,
504              token,
505              fetchImpl: fetch,
506              localStore: localStorage,
507              sessionStore: sessionStorage,
508            });
509            return;
510          }
511          if (!token) return;
512          try {
513            await fetch(apiBase + '/api/v1/auth/logout', {
514              method: 'POST',
515              headers: { Authorization: 'Bearer ' + token },
516              credentials: 'include',
517            });
518          } catch (_) {
519            // Local logout still succeeds if backend is unreachable.
520          }
521        }
522
523        async function clearCartOnLogout(token) {
524          const headers = {};
525          if (token) {
526            headers.Authorization = 'Bearer ' + token;
527          }
528          try {
529            await fetch(apiBase + '/api/v1/cart', {
530              method: 'DELETE',
531              headers,
532              credentials: 'include',
533            });
534          } catch (_) {}
535
536          try { localStorage.removeItem('cart'); } catch (_) {}
537          try { sessionStorage.removeItem('cart'); } catch (_) {}
538          try { localStorage.removeItem('guidasphere.checkout.draft.v1'); } catch (_) {}
539          try { localStorage.removeItem('guidasphere.checkout.idempotencyKey'); } catch (_) {}
540          try { sessionStorage.removeItem('guidasphere.checkout.draft.v1'); } catch (_) {}
541          try { sessionStorage.removeItem('guidasphere.checkout.idempotencyKey'); } catch (_) {}
542
543          try {
544            window.dispatchEvent(new CustomEvent('guidasphere:cart-updated', {
545              detail: { source: 'logout', count: 0, totalItems: 0 },
546            }));
547          } catch (_) {}
548        }
549
550        function clearRuntimePanels() {
551          if (lifecycleController && typeof lifecycleController.clearPanels === 'function') {
552            lifecycleController.clearPanels();
553          }
554        }
555
556        function clearAuthStorage() {
557          if (typeof runtimeAuth.clearAuthStorage === 'function') {
558            runtimeAuth.clearAuthStorage({
559              localStore: localStorage,
560              sessionStore: sessionStorage,
561              onAfterClear: () => {
562                removeRoleBridgeNav();
563                clearRuntimePanels();
564              },
565            });
566            return;
567          }
568          localStorage.removeItem(AUTH_TOKEN_KEY);
569          sessionStorage.removeItem(AUTH_TOKEN_KEY);
570          localStorage.removeItem(AUTH_REFRESH_KEY);
571          sessionStorage.removeItem(AUTH_REFRESH_KEY);
572          localStorage.removeItem(AUTH_USER_KEY);
573          sessionStorage.removeItem(AUTH_USER_KEY);
574          
575          removeRoleBridgeNav();
576          clearRuntimePanels();
577        }
578
579        function getStoredUser() {
580          if (typeof runtimeHelpers.getStoredUser === 'function') {
581            return runtimeHelpers.getStoredUser(localStorage, sessionStorage);
582          }
583          
584          let fromStore = localStorage.getItem(AUTH_USER_KEY) || sessionStorage.getItem(AUTH_USER_KEY);
585          if (fromStore) {
586            try {
587              const parsed = JSON.parse(fromStore);
588              if (parsed && typeof parsed === 'object') return parsed;
589            } catch (_) {}
590          }
591          return null;
592        }
593
594        function isPathMatch(pathname, prefix) {
595          if (typeof runtimeHelpers.isPathMatch === 'function') {
596            return runtimeHelpers.isPathMatch(pathname, prefix);
597          }
598          return pathname === prefix || pathname.startsWith(prefix + '/');
599        }
600
601        function hasAdminPanelAccess(user) {
602          if (!user) return false;
603          if (Array.isArray(user.assignedRoles)) {
604            const roles = user.assignedRoles.map(r => String(r || '').toLowerCase());
605            // Specialized agents should not have broad admin panel access
606            return roles.includes('admin') || roles.includes('ops_manager');
607          }
608          const role = (typeof user.role === 'string') ? user.role.toLowerCase() : '';
609          return role === 'admin' || role === 'ops_manager';
610        }
611
612        function roleBridgeTarget(pathname) {
613          if (typeof runtimeRoleBridge.roleBridgeTarget === 'function') {
614            return runtimeRoleBridge.roleBridgeTarget({
615              pathname,
616              user: getStoredUser(),
617              isPathMatch,
618            });
619          }
620          const user = getStoredUser();
621          const hasAdmin = hasAdminPanelAccess(user);
622          const role = (user && typeof user.role === 'string') ? user.role.toLowerCase() : '';
623
624          if (hasAdmin) {
625            if (isPathMatch(pathname, '/admin')) return { href: '/account', label: 'My Account' };
626            if (isPathMatch(pathname, '/account')) return { href: user.dashboardHome || '/admin', label: 'Admin Dashboard' };
627          }
628
629          if (role === 'seller') {
630            if (isPathMatch(pathname, '/seller')) return { href: '/account', label: 'My Account' };
631            if (isPathMatch(pathname, '/account')) return { href: '/seller', label: 'Seller Dashboard' };
632          }
633
634          return null;
635        }
636
637        function storedUserRole() {
638          const user = getStoredUser();
639          if (!user) return '';
640          if (Array.isArray(user.assignedRoles)) {
641            const roles = user.assignedRoles.map(r => String(r || '').toLowerCase());
642            if (roles.includes('admin')) return 'admin';
643            if (roles.includes('seller')) return 'seller';
644            if (roles.length > 0 && roles[0] !== 'buyer') return roles[0];
645          }
646          return (typeof user.role === 'string') ? user.role.toLowerCase() : '';
647        }
648
649        function enforceSellerRouteAccess(pathname) {
650          if (!isPathMatch(pathname, '/seller')) return true;
651          if (isPathMatch(pathname, '/seller/plans')) return true;
652          const role = storedUserRole();
653          if (role === 'seller' || role === 'admin') return true;
654          window.location.replace('/become-seller');
655          return false;
656        }
657
658        function enforceSpecializedRoleRouteAccess(pathname) {
659          const user = getStoredUser();
660          const roles = Array.isArray(user?.assignedRoles) ? user.assignedRoles.map(r => String(r || '').toLowerCase()) : [];
661          const role = (user && typeof user.role === 'string') ? user.role.toLowerCase() : '';
662          const hasRole = (r) => roles.includes(r) || role === r;
663          const isAdmin = hasAdminPanelAccess(user);
664
665          if (isPathMatch(pathname, '/finance') && !isAdmin && !hasRole('finance_officer')) {
666            persistRedirectNotice('You do not have permission to access the finance area.', 'Access Restricted');
667            window.location.replace(role === 'seller' ? '/seller' : '/account');
668            return false;
669          }
670
671          if (isPathMatch(pathname, '/support') && !isAdmin && !hasRole('support_agent')) {
672            persistRedirectNotice('You do not have permission to access the support area.', 'Access Restricted');
673            window.location.replace(role === 'seller' ? '/seller' : '/account');
674            return false;
675          }
676
677          if (isPathMatch(pathname, '/moderation') && !isAdmin && !hasRole('content_moderator')) {
678            persistRedirectNotice('You do not have permission to access the moderation area.', 'Access Restricted');
679            window.location.replace(role === 'seller' ? '/seller' : '/account');
680            return false;
681          }
682
683          if (isPathMatch(pathname, '/hr') && !isAdmin && !hasRole('hr_manager')) {
684            persistRedirectNotice('You do not have permission to access the HR area.', 'Access Restricted');
685            window.location.replace(role === 'seller' ? '/seller' : '/account');
686            return false;
687          }
688
689          if (isPathMatch(pathname, '/referrals') && !isAdmin && !hasRole('referral_manager')) {
690            persistRedirectNotice('You do not have permission to access the referrals area.', 'Access Restricted');
691            window.location.replace(role === 'seller' ? '/seller' : '/account');
692            return false;
693          }
694
695          return true;
696        }
697
698        function enforceAdminRouteAccess(pathname) {
699          if (!isPathMatch(pathname, '/admin') && !isPathMatch(pathname, '/onboarding-assist-agent')) return true;
700          const user = getStoredUser();
701
702          // Auto-redirect specialized roles to their specific dashboards if hitting /admin root or dashboard
703          if (pathname === '/admin' || pathname === '/admin/' || pathname === '/admin/dashboard' || pathname === '/admin/dashboard/') {
704            const roles = Array.isArray(user?.assignedRoles) ? user.assignedRoles.map(r => String(r || '').toLowerCase()) : [];
705            const role = (user && typeof user.role === 'string') ? user.role.toLowerCase() : '';
706            
707            if (roles.includes('support_agent') || role === 'support_agent') {
708              window.location.replace('/support/tickets');
709              return false;
710            }
711            if (roles.includes('finance_officer') || role === 'finance_officer') {
712              window.location.replace('/finance/overview');
713              return false;
714            }
715            if (roles.includes('content_moderator') || role === 'content_moderator') {
716              window.location.replace('/moderation/queue');
717              return false;
718            }
719            if (roles.includes('hr_manager') || role === 'hr_manager') {
720              window.location.replace('/hr/careers');
721              return false;
722            }
723            if (roles.includes('referral_manager') || role === 'referral_manager') {
724              window.location.replace('/referrals/admin');
725              return false;
726            }
727            if (roles.includes('onboarding_assist_agent') || role === 'onboarding_assist_agent') {
728              window.location.replace('/onboarding-assist-agent');
729              return false;
730            }
731          }
732
733          if (hasAdminPanelAccess(user)) return true;
734
735          if (isPathMatch(pathname, '/admin/onboarding-assist-agent') || isPathMatch(pathname, '/onboarding-assist-agent')) {
736            const roles = Array.isArray(user?.assignedRoles) ? user.assignedRoles.map(r => String(r || '').toLowerCase()) : [];
737            const role = (user && typeof user.role === 'string') ? user.role.toLowerCase() : '';
738            if (roles.includes('onboarding_assist_agent') || role === 'onboarding_assist_agent') return true;
739          }
740
741          const role = storedUserRole();
742          if (role === 'seller') {
743            persistRedirectNotice('You do not have permission to access the admin area.', 'Access Restricted');
744            window.location.replace('/seller');
745            return false;
746          }
747          persistRedirectNotice('You do not have permission to access the admin area.', 'Access Restricted');
748          window.location.replace('/account');
749          return false;
750        }
751
752        function removeRoleBridgeNav() {
753          if (typeof runtimeRoleBridge.removeRoleBridgeNav === 'function') {
754            runtimeRoleBridge.removeRoleBridgeNav({
755              documentLike: document,
756              bridgeId: roleBridgeId,
757            });
758            return;
759          }
760          const existing = document.getElementById(roleBridgeId);
761          if (existing) existing.remove();
762        }
763
764        function renderRoleBridgeNav(pathname) {
765          if (typeof runtimeRoleBridge.renderRoleBridgeNav === 'function') {
766            runtimeRoleBridge.renderRoleBridgeNav({
767              pathname,
768              user: getStoredUser(),
769              documentLike: document,
770              bridgeId: roleBridgeId,
771              isPathMatch,
772            });
773            return;
774          }
775          const target = roleBridgeTarget(pathname);
776          if (!target) {
777            removeRoleBridgeNav();
778            return;
779          }
780
781          let nav = document.getElementById(roleBridgeId);
782          if (!nav) {
783            nav = document.createElement('a');
784            nav.id = roleBridgeId;
785            nav.style.position = 'fixed';
786            nav.style.right = '16px';
787            nav.style.bottom = '16px';
788            nav.style.zIndex = '2147483647';
789            nav.style.padding = '10px 14px';
790            nav.style.borderRadius = '9999px';
791            nav.style.background = '#111827';
792            nav.style.color = '#ffffff';
793            nav.style.fontSize = '13px';
794            nav.style.fontWeight = '600';
795            nav.style.textDecoration = 'none';
796            nav.style.boxShadow = '0 8px 18px rgba(0, 0, 0, 0.24)';
797            nav.style.border = '1px solid rgba(255,255,255,0.16)';
798            nav.style.fontFamily = 'Inter, sans-serif';
799            document.body.appendChild(nav);
800          }
801
802          nav.href = target.href;
803          nav.textContent = target.label;
804        }
805
806        const onboardingApi = typeof runtimeOnboarding.createRuntimeOnboardingApi === 'function'
807          ? runtimeOnboarding.createRuntimeOnboardingApi({
808            apiBase,
809            fetchImpl: fetch,
810            getAccessToken,
811          })
812          : {
813            async getOnboardingState() { return null; },
814            async updateOnboardingProfile() { return null; },
815            async completeOnboarding() { return null; },
816            async listOnboardingCountries() { return { items: [] }; },
817            async listVerificationCountries() { return { items: [] }; },
818            async getSellerPayoutOptions() {
819              return {
820                payout: {
821                  methods: [
822                    { code: 'mtn_momo', label: 'MTN Mobile Money', isActive: true },
823                    { code: 'telecel_cash', label: 'Telecel Cash', isActive: true },
824                    { code: 'airtel_tigo', label: 'AirtelTigo Money', isActive: true },
825                    { code: 'bank_transfer', label: 'Bank Transfer', isActive: true },
826                  ],
827                  banks: [
828                    { name: 'GCB Bank', isActive: true },
829                    { name: 'Ecobank Ghana', isActive: true },
830                    { name: 'Absa Bank Ghana', isActive: true },
831                    { name: 'Fidelity Bank Ghana', isActive: true },
832                  ],
833                },
834                source: 'default',
835              };
836            },
837            async getSellerUpgradeRequest() { return null; },
838            async getSellerUpgradeEligibility() { return null; },
839            async getSellerSettings() {
840              return {
841                profile: {
842                  storeName: 'Seller Store',
843                  storeSlug: '',
844                  tagline: '',
845                  description: '',
846                  bannerUrl: '',
847                  avatarUrl: '',
848                  location: '',
849                  contactPhone: '',
850                  businessEmail: '',
851                },
852                categories: { selected: [], maxSelection: 6 },
853                policies: {
854                  returnPolicy: '',
855                  shippingPolicy: '',
856                  warrantyPolicy: '',
857                  processingTime: '1-2 business days',
858                  shipsFrom: '',
859                },
860                social: {
861                  facebook: '',
862                  instagram: '',
863                  x: '',
864                  tiktok: '',
865                  whatsapp: '',
866                  website: '',
867                },
868              };
869            },
870            async updateSellerSettings() { return null; },
871            async uploadSellerSettingsImage() { return null; },
872            async getSellerOverview() { return null; },
873            async updateSellerOverviewPreferences() { return null; },
874            async listSellerProducts() { return { items: [], count: 0, summary: { all: 0, active: 0, draft: 0, paused: 0 } }; },
875            async listSellerInventory() {
876              return {
877                items: [],
878                count: 0,
879                summary: {
880                  totalProducts: 0,
881                  totalSKUs: 0,
882                  inStockCount: 0,
883                  lowStockCount: 0,
884                  outOfStockCount: 0,
885                  totalInventoryValue: 0,
886                  potentialRevenue: 0,
887                  all: 0,
888                  low: 0,
889                  out: 0,
890                  ok: 0,
891                },
892              };
893            },
894            async getSellerInventoryHistory(productIdentifier) {
895              return { productId: String(productIdentifier || ''), history: [] };
896            },
897            async restockSellerInventory() { return { items: [], count: 0 }; },
898            async listSellerPayouts(query) {
899              const qs = query?.status ? `?status=${query.status}` : '';
900              const response = await fetch(apiBase + '/api/v1/seller/payouts' + qs, {
901                headers: { Authorization: 'Bearer ' + token },
902              });
903              if (!response.ok) throw new Error('Failed to load payouts');
904              return await response.json();
905            },
906            async createSellerPayoutAccount(payload) {
907              const response = await fetch(apiBase + '/api/v1/seller/payouts/accounts', {
908                method: 'POST',
909                headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
910                body: JSON.stringify(payload),
911              });
912              const data = await response.json();
913              if (!response.ok) throw new Error(data.detail || 'Failed to create account');
914              return data;
915            },
916            async updateSellerPayoutAccount(accountId, payload) {
917              const response = await fetch(apiBase + `/api/v1/seller/payouts/accounts/${accountId}`, {
918                method: 'PUT',
919                headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
920                body: JSON.stringify(payload),
921              });
922              const data = await response.json();
923              if (!response.ok) throw new Error(data.detail || 'Failed to update account');
924              return data;
925            },
926            async deleteSellerPayoutAccount(accountId) {
927              const response = await fetch(apiBase + `/api/v1/seller/payouts/accounts/${accountId}`, {
928                method: 'DELETE',
929                headers: { Authorization: 'Bearer ' + token },
930              });
931              const data = await response.json();
932              if (!response.ok) throw new Error(data.detail || 'Failed to delete account');
933              return data;
934            },
935            async createSellerPayoutRequest(payload) {
936              const response = await fetch(apiBase + '/api/v1/seller/payouts/requests', {
937                method: 'POST',
938                headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
939                body: JSON.stringify(payload),
940              });
941              const data = await response.json();
942              if (!response.ok) throw new Error(data.detail || 'Failed to request payout');
943              return data;
944            },
945            async getFinanceSummary(period) {
946              const qs = period ? `?period=${period}` : '';
947              const response = await fetch(apiBase + '/api/v1/finance/summary' + qs, {
948                headers: { Authorization: 'Bearer ' + token },
949              });
950              if (!response.ok) throw new Error('Failed to load finance summary');
951              return await response.json();
952            },
953            async listFinancePayouts(status) {
954              const qs = status && typeof status === 'string' ? `?status=${status}` : '';
955              const response = await fetch(apiBase + '/api/v1/finance/payouts' + qs, {
956                headers: { Authorization: 'Bearer ' + token },
957              });
958              if (!response.ok) throw new Error('Failed to load finance payouts');
959              return await response.json();
960            },
961            async updatePayoutStatus(payoutId, payload) {
962              const response = await fetch(apiBase + `/api/v1/finance/payouts/${payoutId}/status`, {
963                method: 'PUT',
964                headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
965                body: JSON.stringify(payload),
966              });
967              if (!response.ok) throw new Error('Failed to update payout status');
968              return await response.json();
969            },
970            async listFinanceSellers(page = 1, search = '') {
971              const url = new URL(apiBase + '/api/v1/finance/sellers');
972              if (page) url.searchParams.append('page', page);
973              if (search) url.searchParams.append('q', search);
974              const response = await fetch(url.toString(), { headers: { Authorization: 'Bearer ' + token } });
975              if (!response.ok) throw new Error('Failed to list sellers');
976              return await response.json();
977            },
978            async listFinanceMessages(page = 1, search = '', status = 'all') {
979              const url = new URL(apiBase + '/api/v1/finance/messages/sellers');
980              if (page) url.searchParams.append('page', page);
981              if (search) url.searchParams.append('q', search);
982              if (status) url.searchParams.append('status', status);
983              const response = await fetch(url.toString(), { headers: { Authorization: 'Bearer ' + token } });
984              if (!response.ok) throw new Error('Failed to list messages');
985              return await response.json();
986            },
987            async getFinanceConversation(identifier) {
988              const response = await fetch(apiBase + `/api/v1/finance/messages/sellers/${identifier}`, { headers: { Authorization: 'Bearer ' + token } });
989              if (!response.ok) throw new Error('Failed to get conversation');
990              return await response.json();
991            },
992            async sendFinanceMessage(identifier, text) {
993              const response = await fetch(apiBase + `/api/v1/finance/messages/sellers/${identifier}/reply`, {
994                method: 'POST',
995                headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
996                body: JSON.stringify({ message: text })
997              });
998              if (!response.ok) throw new Error('Failed to send message');
999              return await response.json();
1000            },
1001            async startFinanceConversation(sellerUserId, storeId, text) {
1002              const response = await fetch(apiBase + `/api/v1/finance/messages/sellers/send`, {
1003                method: 'POST',
1004                headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
1005                body: JSON.stringify({ 
1006                  sellerUserId: sellerUserId ? Number(sellerUserId) : null,
1007                  storeId: storeId ? Number(storeId) : null,
1008                  text: text,
1009                  channels: ["inbox"]
1010                })
1011              });
1012              if (!response.ok) throw new Error('Failed to start conversation');
1013              return await response.json();
1014            },
1015            async getSellerProduct() { return null; },
1016            async listSellerProductVariants(productId) {
1017              return {
1018                productId: Number(productId || 0),
1019                productName: 'Product',
1020                hasVariants: false,
1021                variants: [],
1022              };
1023            },
1024            async updateSellerProductVariantsEnabled(productId, enabled) {
1025              return {
1026                productId: Number(productId || 0),
1027                productName: 'Product',
1028                hasVariants: Boolean(enabled),
1029                variants: [],
1030              };
1031            },
1032            async createSellerProductVariant(productId) {
1033              return {
1034                productId: Number(productId || 0),
1035                productName: 'Product',
1036                hasVariants: true,
1037                variants: [],
1038                variant: null,
1039              };
1040            },
1041            async updateSellerProductVariant(productId) {
1042              return {
1043                productId: Number(productId || 0),
1044                productName: 'Product',
1045                hasVariants: true,
1046                variants: [],
1047                variant: null,
1048              };
1049            },
1050            async deleteSellerProductVariant(productId) {
1051              return {
1052                productId: Number(productId || 0),
1053                productName: 'Product',
1054                hasVariants: false,
1055                variants: [],
1056              };
1057            },
1058            async createSellerProduct() { return null; },
1059            async updateSellerProduct() { return null; },
1060            async updateSellerProductStatus() { return null; },
1061            async deleteSellerProduct() { return null; },
1062            async listSellerOrders() {
1063              return {
1064                items: [],
1065                count: 0,
1066                summary: {
1067                  all: 0,
1068                  pending: 0,
1069                  processing: 0,
1070                  shipped: 0,
1071                  delivered: 0,
1072                  cancelled: 0,
1073                  refunded: 0,
1074                },
1075                unreadMessages: 0,
1076              };
1077            },
1078            async getSellerOrder() { return null; },
1079            async updateSellerOrderStatus() { return null; },
1080            async createSellerOrderMessage() { return null; },
1081            async markSellerOrderMessagesRead() { return null; },
1082            async createBuyerSellerInquiry() { return null; },
1083            async listSellerMessages() {
1084              return {
1085                items: [],
1086                count: 0,
1087                unreadCount: 0,
1088                summary: {
1089                  all: 0,
1090                  unread: 0,
1091                  replied: 0,
1092                  archived: 0,
1093                },
1094              };
1095            },
1096            async getSellerMessage() { return null; },
1097            async replySellerMessage() { return null; },
1098            async markSellerMessageRead() { return null; },
1099            async archiveSellerMessage() { return null; },
1100            async unarchiveSellerMessage() { return null; },
1101            async applySellerUpgrade() { return null; },
1102            async listSellerUpgradeRequests() { return { items: [] }; },
1103            async listAdminSellers() { return { items: [], count: 0 }; },
1104            async getAdminSeller() { return null; },
1105            async updateAdminSeller() { return null; },
1106            async listAdminSellerVerifications() { return { items: [], count: 0 }; },
1107            async getAdminSellerVerificationBySeller() { return { verification: null }; },
1108            async getAdminSellerVerification() { return { verification: null }; },
1109            async reviewAdminSellerVerification() { return null; },
1110            async openSellerVerificationDocument() { return { url: '', contentType: '', fileName: '' }; },
1111            async openAdminSellerVerificationDocument() { return { url: '', contentType: '', fileName: '' }; },
1112            async listAdminUsers() { return { items: [], count: 0 }; },
1113            async getAdminReports() {
1114              return {
1115                period: '12m',
1116                summary: {
1117                  totalRevenue: 0,
1118                  totalOrders: 0,
1119                  avgOrderValue: 0,
1120                  revenueGrowth: 0,
1121                  ordersGrowth: 0,
1122                  activeSellers: 0,
1123                  newSellersThisMonth: 0,
1124                  platformCommission: 0,
1125                  currency: 'GHS',
1126                },
1127                monthly: [],
1128                daily: [],
1129                categories: [],
1130                topSellers: [],
1131              };
1132            },
1133            async updateAdminUser() { return null; },
1134            async supportAdminUserPassword() { return null; },
1135            async listAdminCountries() { return { items: [] }; },
1136            async getAdminSmtpConfiguration() { return null; },
1137            async updateAdminSmtpConfiguration() { return null; },
1138            async getAdminEmailTemplateConfiguration() { return null; },
1139            async updateAdminEmailTemplateConfiguration() { return null; },
1140            async updateAdminCountry() { return null; },
1141            async approveSellerUpgrade() { return null; },
1142            async rejectSellerUpgrade() { return null; },
1143          };
1144        window.GUIDASPHERE_ONBOARDING_API = onboardingApi;
1145
1146        const authFlowApi = typeof runtimeAuthFlows.createRuntimeAuthFlows === 'function'
1147          ? runtimeAuthFlows.createRuntimeAuthFlows({
1148            apiBase,
1149            fetchImpl: fetch,
1150            windowLike: window,
1151            localStore: localStorage,
1152            sessionStore: sessionStorage,
1153            getAccessToken,
1154            onboardingApi,
1155          })
1156          : {
1157            async signup() {
1158              throw new Error('Could not create account');
1159            },
1160            async listSignupCountries() {
1161              return { items: [], count: 0 };
1162            },
1163            async signin() {
1164              throw new Error('Could not sign in');
1165            },
1166            async signinWithGoogle() {
1167              throw new Error('Could not sign in with Google');
1168            },
1169            shouldUseGoogleRedirectFlow() {
1170              return false;
1171            },
1172            async consumeGoogleSigninRedirect() {
1173              return { handled: false };
1174            },
1175            async submitSellerUpgrade() {
1176              throw new Error('Could not submit application');
1177            },
1178            async getSellerUpgradeEligibility() {
1179              throw new Error('Could not resolve seller eligibility');
1180            },
1181          };
1182        window.GUIDASPHERE_AUTH_FLOW_API = authFlowApi;
1183        if (typeof authFlowApi.preloadGoogleAuth === 'function') {
1184          void authFlowApi.preloadGoogleAuth().catch(() => {});
1185        }
1186        void consumeGoogleSigninRedirectIfPresent();
1187
1188        lifecycleController = typeof runtimeLifecycle.createRuntimeLifecycle === 'function'
1189          ? runtimeLifecycle.createRuntimeLifecycle({
1190            windowLike: window,
1191            documentLike: document,
1192            historyLike: history,
1193            apiBase,
1194            onboardingApi,
1195            getStoredUser,
1196            isPathMatch,
1197            getAccessToken,
1198          })
1199          : null;
1200
1201        function scheduleLifecycleUiRefresh(pathname) {
1202          if (lifecycleController && typeof lifecycleController.scheduleLifecycleUiRefresh === 'function') {
1203            lifecycleController.scheduleLifecycleUiRefresh(pathname);
1204          }
1205        }
1206
1207        function scheduleCatalogPageRefresh(pathname) {
1208          if (catalogPagesController && typeof catalogPagesController.scheduleRefresh === 'function') {
1209            catalogPagesController.scheduleRefresh(pathname);
1210          }
1211        }
1212
1213        let lifecycleSignalRefreshTimer = null;
1214        function queueLifecycleRefresh() {
1215          if (lifecycleSignalRefreshTimer) {
1216            window.clearTimeout(lifecycleSignalRefreshTimer);
1217          }
1218          lifecycleSignalRefreshTimer = window.setTimeout(() => {
1219            lifecycleSignalRefreshTimer = null;
1220            const pathname = window.location.pathname;
1221            scheduleLifecycleUiRefresh(pathname);
1222            scheduleCatalogPageRefresh(pathname);
1223            renderRoleBridgeNav(pathname);
1224            void enforceProtectedRoute(pathname);
1225          }, 40);
1226        }
1227
1228
1229        function isProtectedPath(pathname) {
1230          if (typeof runtimeHelpers.isProtectedPath === 'function') {
1231            return runtimeHelpers.isProtectedPath(pathname, protectedPrefixes);
1232          }
1233          return protectedPrefixes.some((prefix) => pathname === prefix || pathname.startsWith(prefix + '/'));
1234        }
1235
1236        async function tokenStillValid(token) {
1237          if (typeof runtimeAuth.tokenStillValid === 'function') {
1238            return await runtimeAuth.tokenStillValid({
1239              apiBase,
1240              token,
1241              fetchImpl: fetch,
1242              localStore: localStorage,
1243              sessionStore: sessionStorage,
1244              windowLike: window,
1245            });
1246          }
1247          if (!token) return false;
1248          try {
1249            const response = await fetch(apiBase + '/api/v1/auth/me', {
1250              method: 'GET',
1251              headers: {
1252                Accept: 'application/json',
1253                Authorization: 'Bearer ' + token,
1254              },
1255              credentials: 'include',
1256            });
1257            if (!response.ok) return false;
1258
1259            const me = await response.json().catch(() => null);
1260            if (me && typeof me === 'object') {
1261              const serialized = JSON.stringify(me);
1262              const store = sessionStorage.getItem(AUTH_TOKEN_KEY) ? sessionStorage : localStorage;
1263              store.setItem(AUTH_USER_KEY, serialized);
1264            }
1265
1266            return true;
1267          } catch (_) {
1268            return false;
1269          }
1270        }
1271
1272        async function enforceProtectedRoute(pathname) {
1273          if (typeof runtimeAuth.enforceProtectedRoute === 'function') {
1274            const result = await runtimeAuth.enforceProtectedRoute({
1275              pathname,
1276              isProtectedPath,
1277              getAccessToken,
1278              clearAuthStorage,
1279              tokenStillValid,
1280              onAuthenticated: (path) => renderRoleBridgeNav(path || pathname),
1281              onRedirectToSignin: (path) => {
1282                if (path !== '/signin') {
1283                  window.location.replace(signinRedirectUrl(path));
1284                }
1285              },
1286            });
1287            if (result && result.protected && result.authenticated) {
1288              if (!enforceAdminRouteAccess(pathname)) {
1289                return { protected: true, authenticated: true, redirected: true };
1290              }
1291              if (!enforceSpecializedRoleRouteAccess(pathname)) {
1292                return { protected: true, authenticated: true, redirected: true };
1293              }
1294              if (!enforceSellerRouteAccess(pathname)) {
1295                return { protected: true, authenticated: true, redirected: true };
1296              }
1297            }
1298            return result;
1299          }
1300          if (!isProtectedPath(pathname)) return;
1301
1302          const token = getAccessToken();
1303          if (!token) {
1304            clearAuthStorage();
1305            if (pathname !== '/signin') {
1306              window.location.replace(signinRedirectUrl(pathname));
1307            }
1308            return;
1309          }
1310
1311          const valid = await tokenStillValid(token);
1312          if (!valid) {
1313            clearAuthStorage();
1314            if (pathname !== '/signin') {
1315              window.location.replace(signinRedirectUrl(pathname));
1316            }
1317            return;
1318          }
1319
1320          renderRoleBridgeNav(pathname);
1321          if (!enforceAdminRouteAccess(pathname)) return;
1322          if (!enforceSpecializedRoleRouteAccess(pathname)) return;
1323          enforceSellerRouteAccess(pathname);
1324        }
1325
1326        function pathFromUrl(url) {
1327          if (typeof runtimeHelpers.pathFromUrl === 'function') {
1328            return runtimeHelpers.pathFromUrl(url, window);
1329          }
1330          if (!url) return window.location.pathname;
1331          try {
1332            return new URL(url, window.location.origin).pathname;
1333          } catch (_) {
1334            return window.location.pathname;
1335          }
1336        }
1337
1338        function isLogoutTrigger(element) {
1339          if (typeof runtimeHelpers.isLogoutTrigger === 'function') {
1340            return runtimeHelpers.isLogoutTrigger(element);
1341          }
1342          const target = element.closest('button,a');
1343          if (!target) return false;
1344
1345          const text = (target.textContent || '').trim().toLowerCase();
1346          if (text === 'logout' || text === 'log out' || text === 'sign out' || text === 'signout') return true;
1347
1348          return Boolean(target.querySelector('.ri-logout-box-line,.ri-logout-box-r-line,.ri-logout-circle-line'));
1349        }
1350
1351        async function handleLogoutClick(event) {
1352          const target = event.target instanceof Element ? event.target : null;
1353          if (!target || !isLogoutTrigger(target)) return;
1354
1355          event.preventDefault();
1356          event.stopPropagation();
1357
1358          const token = getAccessToken();
1359          await clearCartOnLogout(token);
1360          await callLogout(token);
1361          clearAuthStorage();
1362          window.location.assign('/signin');
1363        }
1364
1365        function isGoogleAuthTrigger(element) {
1366          const button = element?.closest?.('button');
1367          if (!button) return false;
1368          const text = String(button.textContent || '').replace(/\s+/g, ' ').trim().toLowerCase();
1369          return text === 'continue with google' || text === 'sign up with google';
1370        }
1371
1372        function warmupGoogleAuth() {
1373          const preload = window.GUIDASPHERE_AUTH_FLOW_API?.preloadGoogleAuth;
1374          if (typeof preload === 'function') {
1375            void preload().catch(() => {});
1376          }
1377        }
1378
1379        function sanitizeNextPath(value) {
1380          const token = String(value || '').trim();
1381          if (!token) return '';
1382          if (!token.startsWith('/')) return '';
1383          if (token.startsWith('//')) return '';
1384          if (/^[a-z][a-z0-9+.-]*:/i.test(token)) return '';
1385          const normalized = token.toLowerCase();
1386          if (normalized === '/signin' || normalized.startsWith('/signin?') || normalized.startsWith('/signin#')) {
1387            return '';
1388          }
1389          if (normalized === '/signup' || normalized.startsWith('/signup?') || normalized.startsWith('/signup#')) {
1390            return '';
1391          }
1392          return token;
1393        }
1394
1395        function readNextPathFromLocation() {
1396          try {
1397            const url = new URL(window.location.href);
1398            return sanitizeNextPath(url.searchParams.get('next'));
1399          } catch (_) {
1400            return '';
1401          }
1402        }
1403
1404        function signinRedirectUrl(pathname) {
1405          const nextPath = sanitizeNextPath(pathname);
1406          return nextPath ? `/signin?next=${encodeURIComponent(nextPath)}` : '/signin';
1407        }
1408
1409        function routeToSignedInHome(result) {
1410          const nextPath = readNextPathFromLocation();
1411          if (nextPath) {
1412            window.location.assign(nextPath);
1413            return;
1414          }
1415          const role = String(result?.user?.role || '').toLowerCase();
1416          if (role === 'admin') {
1417            window.location.assign('/admin');
1418          } else if (role === 'seller') {
1419            window.location.assign('/seller');
1420          } else {
1421            window.location.assign('/account');
1422          }
1423        }
1424
1425        function handleGoogleTwoFactorChallenge(result) {
1426          if (!result?.twoFactorRequired || !result?.challenge?.challengeId) {
1427            return false;
1428          }
1429          const challengeDetail = {
1430            source: 'google',
1431            rememberMe: true,
1432            challenge: result.challenge,
1433          };
1434          if (window.location.pathname === '/signin') {
1435            document.dispatchEvent(new CustomEvent('guidasphere:auth-two-factor-required', {
1436              detail: challengeDetail,
1437            }));
1438          } else {
1439            try {
1440              window.sessionStorage.setItem(
1441                'guidasphere.pendingTwoFactorChallenge',
1442                JSON.stringify(challengeDetail),
1443              );
1444            } catch (_) {
1445              // no-op
1446            }
1447            window.location.assign('/signin');
1448          }
1449          return true;
1450        }
1451
1452        async function consumeGoogleSigninRedirectIfPresent() {
1453          if (window.location.pathname !== '/signin' && window.location.pathname !== '/signup') return;
1454          const consumeRedirect = window.GUIDASPHERE_AUTH_FLOW_API?.consumeGoogleSigninRedirect;
1455          if (typeof consumeRedirect !== 'function') return;
1456          try {
1457            const result = await consumeRedirect();
1458            if (!result?.handled) return;
1459            if (result?.error) {
1460              showRuntimeAlert(String(result.error), 'Google Sign-In');
1461              return;
1462            }
1463            if (handleGoogleTwoFactorChallenge(result)) return;
1464            routeToSignedInHome(result);
1465          } catch (error) {
1466            const detail = error?.message ? String(error.message) : 'Could not sign in with Google';
1467            showRuntimeAlert(detail, 'Google Sign-In');
1468          }
1469        }
1470
1471        async function handleGoogleAuthClick(event) {
1472          const target = event.target instanceof Element ? event.target : null;
1473          if (!target || !isGoogleAuthTrigger(target)) return;
1474          if (window.location.pathname !== '/signin' && window.location.pathname !== '/signup') return;
1475
1476          const button = target.closest('button');
1477          if (!button || button.dataset.guidasphereGoogleBusy === '1') return;
1478          event.preventDefault();
1479          event.stopPropagation();
1480
1481          if (!window.GUIDASPHERE_AUTH_FLOW_API?.signinWithGoogle) {
1482            showRuntimeAlert('Google sign-in is not available right now.', 'Google Sign-In');
1483            return;
1484          }
1485
1486          const shouldUseRedirectFlow = window.GUIDASPHERE_AUTH_FLOW_API?.shouldUseGoogleRedirectFlow;
1487          const redirectFlowActive = typeof shouldUseRedirectFlow === 'function'
1488            ? Boolean(shouldUseRedirectFlow())
1489            : false;
1490
1491          const isReady = window.GUIDASPHERE_AUTH_FLOW_API?.isGoogleOauthReady;
1492          if (!redirectFlowActive && typeof isReady === 'function' && !isReady()) {
1493            warmupGoogleAuth();
1494            showRuntimeAlert('Google sign-in is loading. Click Continue with Google again.', 'Google Sign-In');
1495            return;
1496          }
1497
1498          const originalContent = button.innerHTML;
1499          const wasDisabled = button.disabled;
1500          button.dataset.guidasphereGoogleBusy = '1';
1501          button.disabled = true;
1502          button.classList.add('opacity-70');
1503          button.innerHTML = '<i class="ri-loader-4-line animate-spin"></i><span>Connecting...</span>';
1504
1505          try {
1506            const result = await window.GUIDASPHERE_AUTH_FLOW_API.signinWithGoogle({ rememberMe: true });
1507            if (result?.redirecting) return;
1508            if (handleGoogleTwoFactorChallenge(result)) return;
1509            routeToSignedInHome(result);
1510          } catch (error) {
1511            const detail = error?.message ? String(error.message) : 'Could not sign in with Google';
1512            showRuntimeAlert(detail, 'Google Sign-In');
1513          } finally {
1514            button.innerHTML = originalContent;
1515            button.disabled = wasDisabled;
1516            button.classList.remove('opacity-70');
1517            delete button.dataset.guidasphereGoogleBusy;
1518          }
1519        }
1520
1521        if (typeof runtimeNavigation.installRuntimeNavigation === 'function') {
1522          runtimeNavigation.installRuntimeNavigation({
1523            documentLike: document,
1524            windowLike: window,
1525            historyLike: history,
1526            pathFromUrl,
1527            enforceProtectedRoute,
1528            renderRoleBridgeNav,
1529            scheduleLifecycleUiRefresh,
1530            scheduleCatalogPageRefresh,
1531            handleLogoutClick,
1532          });
1533        } else {
1534          document.addEventListener('click', (event) => {
1535            void handleLogoutClick(event);
1536          }, true);
1537
1538          const nativePushState = history.pushState.bind(history);
1539          history.pushState = function pushState(state, title, url) {
1540            nativePushState(state, title, url);
1541            const pathname = pathFromUrl(url);
1542            void enforceProtectedRoute(pathname);
1543            renderRoleBridgeNav(pathname);
1544            scheduleLifecycleUiRefresh(pathname);
1545            scheduleCatalogPageRefresh(pathname);
1546          };
1547
1548          const nativeReplaceState = history.replaceState.bind(history);
1549          history.replaceState = function replaceState(state, title, url) {
1550            nativeReplaceState(state, title, url);
1551            const pathname = pathFromUrl(url);
1552            void enforceProtectedRoute(pathname);
1553            renderRoleBridgeNav(pathname);
1554            scheduleLifecycleUiRefresh(pathname);
1555            scheduleCatalogPageRefresh(pathname);
1556          };
1557
1558          window.addEventListener('popstate', () => {
1559            const pathname = window.location.pathname;
1560            void enforceProtectedRoute(pathname);
1561            renderRoleBridgeNav(pathname);
1562            scheduleLifecycleUiRefresh(pathname);
1563            scheduleCatalogPageRefresh(pathname);
1564          });
1565        }
1566
1567        document.addEventListener('click', (event) => {
1568          void handleGoogleAuthClick(event);
1569        }, true);
1570        document.addEventListener('pointerdown', (event) => {
1571          const target = event.target instanceof Element ? event.target : null;
1572          if (!target || !isGoogleAuthTrigger(target)) return;
1573          warmupGoogleAuth();
1574        }, true);
1575        document.addEventListener('focusin', (event) => {
1576          const target = event.target instanceof Element ? event.target : null;
1577          if (!target || !isGoogleAuthTrigger(target)) return;
1578          warmupGoogleAuth();
1579        }, true);
1580
1581        window.addEventListener('guidasphere:onboarding-updated', 
1581() => {
1582          queueLifecycleRefresh();
1583        });
1584        window.addEventListener('guidasphere:auth-updated', () => {
1585          queueLifecycleRefresh();
1586        });
1587        document.addEventListener('visibilitychange', () => {
1588          if (document.visibilityState === 'visible') {
1589            queueLifecycleRefresh();
1590          }
1591        });
1592
1593        // -----------------------------------------------------------------------------
1594        // Safari / ITP Cross-Site Navigation Fix
1595        // -----------------------------------------------------------------------------
1596        window.preserveAuthStateForNavigation = function() {
1597          const accessToken = localStorage.getItem(AUTH_TOKEN_KEY) || sessionStorage.getItem(AUTH_TOKEN_KEY);
1598          if (!accessToken) return; // Nothing to preserve
1599
1600          const refreshToken = localStorage.getItem(AUTH_REFRESH_KEY) || sessionStorage.getItem(AUTH_REFRESH_KEY) || '';
1601          const authUser = localStorage.getItem(AUTH_USER_KEY) || sessionStorage.getItem(AUTH_USER_KEY) || '';
1602          const persistToSession = Boolean(sessionStorage.getItem(AUTH_TOKEN_KEY)); // Where did it come from?
1603
1604          const backup = {
1605            accessToken,
1606            refreshToken,
1607            authUser,
1608            persistToSession
1609          };
1610          localStorage.setItem(AUTH_BACKUP_KEY, JSON.stringify(backup));
1611        };
1612
1613        function restoreAuthStateFromNavigation() {
1614          const backupStr = localStorage.getItem(AUTH_BACKUP_KEY);
1615          if (!backupStr) return;
1616          
1617          try {
1618            const backup = JSON.parse(backupStr);
1619            localStorage.removeItem(AUTH_BACKUP_KEY);
1620
1621            // Only restore if we don't already have an active token in the expected place.
1622            // If the browser wiped sessionStorage on a redirect, this will restore it.
1623            const currentToken = localStorage.getItem(AUTH_TOKEN_KEY) || sessionStorage.getItem(AUTH_TOKEN_KEY);
1624            if (!currentToken && backup.accessToken) {
1625               const targetStore = backup.persistToSession ? sessionStorage : localStorage;
1626               targetStore.setItem(AUTH_TOKEN_KEY, backup.accessToken);
1627               if (backup.refreshToken) targetStore.setItem(AUTH_REFRESH_KEY, backup.refreshToken);
1628               if (backup.authUser) targetStore.setItem(AUTH_USER_KEY, backup.authUser);
1629            }
1630          } catch (_) {
1631            localStorage.removeItem(AUTH_BACKUP_KEY);
1632          }
1633        }
1634
1635        // Run restore on boot
1636        restoreAuthStateFromNavigation();
1637
1638        const currentPathname = window.location.pathname;
1639        void enforceProtectedRoute(currentPathname);
1640        renderRoleBridgeNav(currentPathname);
1641        scheduleLifecycleUiRefresh(currentPathname);
1642        scheduleCatalogPageRefresh(currentPathname);
1643      })();

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.