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.