style: format and clean up injected scripts
- Apply consistent code formatting to GUI injection scripts - Improve code readability with better indentation and spacing - Clean up popup handling logic with enhanced error handling - Standardize OAuth callback and window communication patterns - Optimize color scheme module formatting and structure
- commit
- 67d90d09161408e22a5bafa85b83948494be0bb9
- author
- bamen · · 2025-09-23 16:02 UTC
- committer
- bamen · · 2025-09-23 16:26 UTC
- parent
- 4b59d50
- signature
- unsigned
3 files · +340 -128
M gui/src/injected/gui.ts +12 -5
| @@ -88,14 +88,17 @@ if (!window.__dumber_gui_ready) { | |||
| 88 | 88 | let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events | |
| 89 | 89 | ||
| 90 | 90 | // Request webview ID from Go if it's unknown (needed for stub builds) | |
| 91 | - | if (currentWebViewId === "unknown" || currentWebViewId === "__WEBVIEW_ID__") { | |
| 91 | + | if ( | |
| 92 | + | currentWebViewId === "unknown" || | |
| 93 | + | currentWebViewId === "__WEBVIEW_ID__" | |
| 94 | + | ) { | |
| 92 | 95 | console.log("[workspace] WebView ID unknown, requesting from Go backend"); | |
| 93 | 96 | if (window.webkit?.messageHandlers?.dumber) { | |
| 94 | 97 | window.webkit.messageHandlers.dumber.postMessage( | |
| 95 | 98 | JSON.stringify({ | |
| 96 | 99 | type: "request-webview-id", | |
| 97 | - | payload: { timestamp: Date.now() } | |
| 98 | - | }) | |
| 100 | + | payload: { timestamp: Date.now() }, | |
| 101 | + | }), | |
| 99 | 102 | ); | |
| 100 | 103 | } | |
| 101 | 104 | } | |
| @@ -106,7 +109,10 @@ if (!window.__dumber_gui_ready) { | |||
| 106 | 109 | if (detail.webviewId) { | |
| 107 | 110 | currentWebViewId = detail.webviewId; | |
| 108 | 111 | window.__dumber_webview_id = detail.webviewId; | |
| 109 | - | console.log("[workspace] Received webview ID from Go:", detail.webviewId); | |
| 112 | + | console.log( | |
| 113 | + | "[workspace] Received webview ID from Go:", | |
| 114 | + | detail.webviewId, | |
| 115 | + | ); | |
| 110 | 116 | } | |
| 111 | 117 | }); | |
| 112 | 118 | ||
| @@ -285,7 +291,8 @@ if (!window.__dumber_gui_ready) { | |||
| 285 | 291 | } | |
| 286 | 292 | ||
| 287 | 293 | // Enhanced focus checking with webview ID validation | |
| 288 | - | const isForThisWebView = !eventWebViewId || eventWebViewId === currentWebViewId; | |
| 294 | + | const isForThisWebView = | |
| 295 | + | !eventWebViewId || eventWebViewId === currentWebViewId; | |
| 289 | 296 | // Only apply strict focus checking to omnibox actions, be permissive for others | |
| 290 | 297 | const isOmniboxAction = action.startsWith("omnibox-"); | |
| 291 | 298 | const shouldHandle = | |
| @@ -88,14 +88,17 @@ if (!window.__dumber_gui_ready) { | |||||
| 88 | let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events | 88 | let hasReceivedFocusEvent = false; // Track if we've received any workspace focus events | ||
| 89 | 89 | ||||
| 90 | // Request webview ID from Go if it's unknown (needed for stub builds) | 90 | // Request webview ID from Go if it's unknown (needed for stub builds) | ||
| 91 | - | if (currentWebViewId === "unknown" || currentWebViewId === "__WEBVIEW_ID__") { | 91 | + | if ( |
| 92 | + | currentWebViewId === "unknown" || | |||
| 93 | + | currentWebViewId === "__WEBVIEW_ID__" | |||
| 94 | + | ) { | |||
| 92 | console.log("[workspace] WebView ID unknown, requesting from Go backend"); | 95 | console.log("[workspace] WebView ID unknown, requesting from Go backend"); | ||
| 93 | if (window.webkit?.messageHandlers?.dumber) { | 96 | if (window.webkit?.messageHandlers?.dumber) { | ||
| 94 | window.webkit.messageHandlers.dumber.postMessage( | 97 | window.webkit.messageHandlers.dumber.postMessage( | ||
| 95 | JSON.stringify({ | 98 | JSON.stringify({ | ||
| 96 | type: "request-webview-id", | 99 | type: "request-webview-id", | ||
| 97 | - | payload: { timestamp: Date.now() } | 100 | + | payload: { timestamp: Date.now() }, |
| 98 | - | }) | 101 | + | }), |
| 99 | ); | 102 | ); | ||
| 100 | } | 103 | } | ||
| 101 | } | 104 | } | ||
| @@ -106,7 +109,10 @@ if (!window.__dumber_gui_ready) { | |||||
| 106 | if (detail.webviewId) { | 109 | if (detail.webviewId) { | ||
| 107 | currentWebViewId = detail.webviewId; | 110 | currentWebViewId = detail.webviewId; | ||
| 108 | window.__dumber_webview_id = detail.webviewId; | 111 | window.__dumber_webview_id = detail.webviewId; | ||
| 109 | - | console.log("[workspace] Received webview ID from Go:", detail.webviewId); | 112 | + | console.log( |
| 113 | + | "[workspace] Received webview ID from Go:", | |||
| 114 | + | detail.webviewId, | |||
| 115 | + | ); | |||
| 110 | } | 116 | } | ||
| 111 | }); | 117 | }); | ||
| 112 | 118 | ||||
| @@ -285,7 +291,8 @@ if (!window.__dumber_gui_ready) { | |||||
| 285 | } | 291 | } | ||
| 286 | 292 | ||||
| 287 | // Enhanced focus checking with webview ID validation | 293 | // Enhanced focus checking with webview ID validation | ||
| 288 | - | const isForThisWebView = !eventWebViewId || eventWebViewId === currentWebViewId; | 294 | + | const isForThisWebView = |
| 295 | + | !eventWebViewId || eventWebViewId === currentWebViewId; | |||
| 289 | // Only apply strict focus checking to omnibox actions, be permissive for others | 296 | // Only apply strict focus checking to omnibox actions, be permissive for others | ||
| 290 | const isOmniboxAction = action.startsWith("omnibox-"); | 297 | const isOmniboxAction = action.startsWith("omnibox-"); | ||
| 291 | const shouldHandle = | 298 | const shouldHandle = | ||
M gui/src/injected/main-world.ts +225 -112
| @@ -84,7 +84,12 @@ function detectWindowType(features?: string | null): string { | |||
| 84 | 84 | const hasNoLocation = /location=0|location=no/.test(featuresStr); | |
| 85 | 85 | ||
| 86 | 86 | // OAuth/login popups typically have size constraints and disabled UI elements | |
| 87 | - | const popupIndicators = [hasSize, hasNoToolbar, hasNoMenubar, hasNoLocation].filter(Boolean).length; | |
| 87 | + | const popupIndicators = [ | |
| 88 | + | hasSize, | |
| 89 | + | hasNoToolbar, | |
| 90 | + | hasNoMenubar, | |
| 91 | + | hasNoLocation, | |
| 92 | + | ].filter(Boolean).length; | |
| 88 | 93 | ||
| 89 | 94 | // If 2 or more popup indicators are present, treat as popup | |
| 90 | 95 | if (popupIndicators >= 2) { | |
| @@ -302,14 +307,19 @@ function detectWindowType(features?: string | null): string { | |||
| 302 | 307 | const findParentPopupId = () => { | |
| 303 | 308 | const keys = Object.keys(localStorage); | |
| 304 | 309 | for (const key of keys) { | |
| 305 | - | if (key.startsWith('popup_') && key.endsWith('_parent_info')) { | |
| 306 | - | const popupId = key.replace('popup_', '').replace('_parent_info', ''); | |
| 307 | - | const parentInfo = JSON.parse(localStorage.getItem(key) || '{}'); | |
| 310 | + | if (key.startsWith("popup_") && key.endsWith("_parent_info")) { | |
| 311 | + | const popupId = key | |
| 312 | + | .replace("popup_", "") | |
| 313 | + | .replace("_parent_info", ""); | |
| 314 | + | const parentInfo = JSON.parse(localStorage.getItem(key) || "{}"); | |
| 308 | 315 | ||
| 309 | 316 | // Check if this popup info is recent (within last 30 seconds) | |
| 310 | 317 | const age = Date.now() - (parentInfo.timestamp || 0); | |
| 311 | 318 | if (age < 30000) { | |
| 312 | - | console.log(`[dumber-popup] Found parent info for popup ID: ${popupId}`, parentInfo); | |
| 319 | + | console.log( | |
| 320 | + | `[dumber-popup] Found parent info for popup ID: ${popupId}`, | |
| 321 | + | parentInfo, | |
| 322 | + | ); | |
| 313 | 323 | return { popupId, parentInfo }; | |
| 314 | 324 | } | |
| 315 | 325 | } | |
| @@ -321,55 +331,78 @@ function detectWindowType(features?: string | null): string { | |||
| 321 | 331 | if (parentData && !window.opener) { | |
| 322 | 332 | const { popupId, parentInfo } = parentData; | |
| 323 | 333 | ||
| 324 | - | console.log(`[dumber-popup] Setting up window.opener bridge for popup ID: ${popupId}`); | |
| 334 | + | console.log( | |
| 335 | + | `[dumber-popup] Setting up window.opener bridge for popup ID: ${popupId}`, | |
| 336 | + | ); | |
| 325 | 337 | ||
| 326 | 338 | // Create window.opener proxy that uses localStorage for communication | |
| 327 | 339 | window.opener = { | |
| 328 | 340 | postMessage: (data: unknown, targetOrigin?: string) => { | |
| 329 | 341 | try { | |
| 330 | - | localStorage.setItem(`popup_${popupId}_message_to_parent`, JSON.stringify({ | |
| 342 | + | localStorage.setItem( | |
| 343 | + | `popup_${popupId}_message_to_parent`, | |
| 344 | + | JSON.stringify({ | |
| 345 | + | data, | |
| 346 | + | origin: targetOrigin || "*", | |
| 347 | + | timestamp: Date.now(), | |
| 348 | + | source: "popup", | |
| 349 | + | }), | |
| 350 | + | ); | |
| 351 | + | console.log( | |
| 352 | + | `[dumber-popup] Sent message to parent via localStorage:`, | |
| 331 | 353 | data, | |
| 332 | - | origin: targetOrigin || '*', | |
| 333 | - | timestamp: Date.now(), | |
| 334 | - | source: 'popup', | |
| 335 | - | })); | |
| 336 | - | console.log(`[dumber-popup] Sent message to parent via localStorage:`, data); | |
| 354 | + | ); | |
| 337 | 355 | } catch (err) { | |
| 338 | - | console.warn(`[dumber-popup] Failed to send message to parent:`, err); | |
| 356 | + | console.warn( | |
| 357 | + | `[dumber-popup] Failed to send message to parent:`, | |
| 358 | + | err, | |
| 359 | + | ); | |
| 339 | 360 | } | |
| 340 | 361 | }, | |
| 341 | 362 | ||
| 342 | 363 | focus: () => { | |
| 343 | 364 | try { | |
| 344 | - | localStorage.setItem(`popup_${popupId}_parent_action`, JSON.stringify({ | |
| 345 | - | action: 'focus', | |
| 346 | - | timestamp: Date.now(), | |
| 347 | - | })); | |
| 365 | + | localStorage.setItem( | |
| 366 | + | `popup_${popupId}_parent_action`, | |
| 367 | + | JSON.stringify({ | |
| 368 | + | action: "focus", | |
| 369 | + | timestamp: Date.now(), | |
| 370 | + | }), | |
| 371 | + | ); | |
| 348 | 372 | } catch (err) { | |
| 349 | - | console.warn(`[dumber-popup] Failed to request parent focus:`, err); | |
| 373 | + | console.warn( | |
| 374 | + | `[dumber-popup] Failed to request parent focus:`, | |
| 375 | + | err, | |
| 376 | + | ); | |
| 350 | 377 | } | |
| 351 | 378 | }, | |
| 352 | 379 | ||
| 353 | 380 | blur: () => { | |
| 354 | 381 | try { | |
| 355 | - | localStorage.setItem(`popup_${popupId}_parent_action`, JSON.stringify({ | |
| 356 | - | action: 'blur', | |
| 357 | - | timestamp: Date.now(), | |
| 358 | - | })); | |
| 382 | + | localStorage.setItem( | |
| 383 | + | `popup_${popupId}_parent_action`, | |
| 384 | + | JSON.stringify({ | |
| 385 | + | action: "blur", | |
| 386 | + | timestamp: Date.now(), | |
| 387 | + | }), | |
| 388 | + | ); | |
| 359 | 389 | } catch (err) { | |
| 360 | - | console.warn(`[dumber-popup] Failed to request parent blur:`, err); | |
| 390 | + | console.warn( | |
| 391 | + | `[dumber-popup] Failed to request parent blur:`, | |
| 392 | + | err, | |
| 393 | + | ); | |
| 361 | 394 | } | |
| 362 | 395 | }, | |
| 363 | 396 | ||
| 364 | 397 | location: { | |
| 365 | - | href: parentInfo.parentUrl || '', | |
| 366 | - | origin: new URL(parentInfo.parentUrl || 'about:blank').origin, | |
| 398 | + | href: parentInfo.parentUrl || "", | |
| 399 | + | origin: new URL(parentInfo.parentUrl || "about:blank").origin, | |
| 367 | 400 | }, | |
| 368 | 401 | ||
| 369 | 402 | closed: false, | |
| 370 | 403 | ||
| 371 | 404 | // Support for custom properties/methods that websites might use | |
| 372 | - | [Symbol.for('popup.bridge')]: true, | |
| 405 | + | [Symbol.for("popup.bridge")]: true, | |
| 373 | 406 | } as unknown as Window; | |
| 374 | 407 | ||
| 375 | 408 | // Set up message polling to receive messages from parent | |
| @@ -383,12 +416,15 @@ function detectWindowType(features?: string | null): string { | |||
| 383 | 416 | ||
| 384 | 417 | // Check if message is recent (within last 5 seconds) | |
| 385 | 418 | if (Date.now() - timestamp < 5000) { | |
| 386 | - | console.log(`[dumber-popup] Received message from parent:`, data); | |
| 419 | + | console.log( | |
| 420 | + | `[dumber-popup] Received message from parent:`, | |
| 421 | + | data, | |
| 422 | + | ); | |
| 387 | 423 | ||
| 388 | 424 | // Dispatch as MessageEvent to window | |
| 389 | - | const event = new MessageEvent('message', { | |
| 425 | + | const event = new MessageEvent("message", { | |
| 390 | 426 | data, | |
| 391 | - | origin: origin || parentInfo.parentUrl || '', | |
| 427 | + | origin: origin || parentInfo.parentUrl || "", | |
| 392 | 428 | source: window.opener, | |
| 393 | 429 | }); | |
| 394 | 430 | window.dispatchEvent(event); | |
| @@ -398,7 +434,10 @@ function detectWindowType(features?: string | null): string { | |||
| 398 | 434 | localStorage.removeItem(messageKey); | |
| 399 | 435 | } | |
| 400 | 436 | } catch (err) { | |
| 401 | - | console.warn(`[dumber-popup] Failed to poll for parent messages:`, err); | |
| 437 | + | console.warn( | |
| 438 | + | `[dumber-popup] Failed to poll for parent messages:`, | |
| 439 | + | err, | |
| 440 | + | ); | |
| 402 | 441 | } | |
| 403 | 442 | }; | |
| 404 | 443 | ||
| @@ -411,10 +450,15 @@ function detectWindowType(features?: string | null): string { | |||
| 411 | 450 | } | |
| 412 | 451 | }); | |
| 413 | 452 | ||
| 414 | - | console.log(`[dumber-popup] window.opener bridge established successfully`); | |
| 453 | + | console.log( | |
| 454 | + | `[dumber-popup] window.opener bridge established successfully`, | |
| 455 | + | ); | |
| 415 | 456 | } | |
| 416 | 457 | } catch (err) { | |
| 417 | - | console.warn(`[dumber-popup] Failed to setup window.opener bridge:`, err); | |
| 458 | + | console.warn( | |
| 459 | + | `[dumber-popup] Failed to setup window.opener bridge:`, | |
| 460 | + | err, | |
| 461 | + | ); | |
| 418 | 462 | } | |
| 419 | 463 | }; | |
| 420 | 464 | ||
| @@ -430,10 +474,11 @@ function detectWindowType(features?: string | null): string { | |||
| 430 | 474 | const hasRelevantData = () => { | |
| 431 | 475 | try { | |
| 432 | 476 | const keys = Object.keys(localStorage); | |
| 433 | - | return keys.some(key => | |
| 434 | - | key.startsWith('popup_mapping_') || | |
| 435 | - | key.startsWith('oauth_callback_') || | |
| 436 | - | key.includes('message_to_parent') | |
| 477 | + | return keys.some( | |
| 478 | + | (key) => | |
| 479 | + | key.startsWith("popup_mapping_") || | |
| 480 | + | key.startsWith("oauth_callback_") || | |
| 481 | + | key.includes("message_to_parent"), | |
| 437 | 482 | ); | |
| 438 | 483 | } catch { | |
| 439 | 484 | return false; | |
| @@ -451,19 +496,23 @@ function detectWindowType(features?: string | null): string { | |||
| 451 | 496 | ||
| 452 | 497 | for (const key of keys) { | |
| 453 | 498 | // Handle popup messages to parent | |
| 454 | - | if (key.includes('message_to_parent')) { | |
| 499 | + | if (key.includes("message_to_parent")) { | |
| 455 | 500 | const messageData = localStorage.getItem(key); | |
| 456 | 501 | if (!messageData) continue; | |
| 457 | 502 | ||
| 458 | 503 | try { | |
| 459 | - | const { data, origin, timestamp, source } = JSON.parse(messageData); | |
| 504 | + | const { data, origin, timestamp, source } = | |
| 505 | + | JSON.parse(messageData); | |
| 460 | 506 | ||
| 461 | 507 | // Check if message is recent (within last 5 seconds) and from a popup | |
| 462 | - | if (Date.now() - timestamp < 5000 && source === 'popup') { | |
| 463 | - | console.log(`[dumber-parent] Received message from popup:`, data); | |
| 508 | + | if (Date.now() - timestamp < 5000 && source === "popup") { | |
| 509 | + | console.log( | |
| 510 | + | `[dumber-parent] Received message from popup:`, | |
| 511 | + | data, | |
| 512 | + | ); | |
| 464 | 513 | ||
| 465 | 514 | // Dispatch as MessageEvent to parent window | |
| 466 | - | const event = new MessageEvent('message', { | |
| 515 | + | const event = new MessageEvent("message", { | |
| 467 | 516 | data, | |
| 468 | 517 | origin: origin || window.location.origin, | |
| 469 | 518 | source: null, // popup reference would be here in real browser | |
| @@ -474,54 +523,75 @@ function detectWindowType(features?: string | null): string { | |||
| 474 | 523 | localStorage.removeItem(key); | |
| 475 | 524 | } | |
| 476 | 525 | } catch (err) { | |
| 477 | - | console.warn(`[dumber-parent] Failed to parse popup message:`, err); | |
| 526 | + | console.warn( | |
| 527 | + | `[dumber-parent] Failed to parse popup message:`, | |
| 528 | + | err, | |
| 529 | + | ); | |
| 478 | 530 | localStorage.removeItem(key); // Clean up invalid message | |
| 479 | 531 | } | |
| 480 | 532 | } | |
| 481 | 533 | ||
| 482 | 534 | // Handle OAuth callback detection from popups | |
| 483 | - | if (key.startsWith('oauth_callback_')) { | |
| 535 | + | if (key.startsWith("oauth_callback_")) { | |
| 484 | 536 | const callbackData = localStorage.getItem(key); | |
| 485 | 537 | if (!callbackData) continue; | |
| 486 | 538 | ||
| 487 | 539 | try { | |
| 488 | - | const { url, webviewId, timestamp, isOAuthCallback } = JSON.parse(callbackData); | |
| 540 | + | const { url, webviewId, timestamp, isOAuthCallback } = | |
| 541 | + | JSON.parse(callbackData); | |
| 489 | 542 | ||
| 490 | 543 | // Check if callback is recent (within last 10 seconds) and is an OAuth callback | |
| 491 | 544 | if (Date.now() - timestamp < 10000 && isOAuthCallback) { | |
| 492 | - | console.log(`[dumber-parent] OAuth callback detected for webview ${webviewId}:`, url); | |
| 545 | + | console.log( | |
| 546 | + | `[dumber-parent] OAuth callback detected for webview ${webviewId}:`, | |
| 547 | + | url, | |
| 548 | + | ); | |
| 493 | 549 | ||
| 494 | 550 | // Send close request to backend for this popup webview | |
| 495 | 551 | const bridge = window.webkit?.messageHandlers?.dumber; | |
| 496 | - | if (bridge && typeof bridge.postMessage === 'function') { | |
| 552 | + | if (bridge && typeof bridge.postMessage === "function") { | |
| 497 | 553 | try { | |
| 498 | 554 | const closeMessage = { | |
| 499 | - | type: 'close-popup', | |
| 555 | + | type: "close-popup", | |
| 500 | 556 | webviewId, | |
| 501 | - | reason: 'oauth-callback-success', | |
| 502 | - | timestamp: Date.now() | |
| 557 | + | reason: "oauth-callback-success", | |
| 558 | + | timestamp: Date.now(), | |
| 503 | 559 | }; | |
| 504 | 560 | ||
| 505 | - | console.log(`[dumber-parent] Sending popup close request:`, closeMessage); | |
| 561 | + | console.log( | |
| 562 | + | `[dumber-parent] Sending popup close request:`, | |
| 563 | + | closeMessage, | |
| 564 | + | ); | |
| 506 | 565 | bridge.postMessage(JSON.stringify(closeMessage)); | |
| 507 | 566 | } catch (err) { | |
| 508 | - | console.warn(`[dumber-parent] Failed to send popup close request:`, err); | |
| 567 | + | console.warn( | |
| 568 | + | `[dumber-parent] Failed to send popup close request:`, | |
| 569 | + | err, | |
| 570 | + | ); | |
| 509 | 571 | } | |
| 510 | 572 | } else { | |
| 511 | - | console.warn(`[dumber-parent] No webkit bridge available for popup close request`); | |
| 573 | + | console.warn( | |
| 574 | + | `[dumber-parent] No webkit bridge available for popup close request`, | |
| 575 | + | ); | |
| 512 | 576 | } | |
| 513 | 577 | ||
| 514 | 578 | // Clean up OAuth callback data | |
| 515 | 579 | localStorage.removeItem(key); | |
| 516 | 580 | } | |
| 517 | 581 | } catch (err) { | |
| 518 | - | console.warn(`[dumber-parent] Failed to parse OAuth callback data:`, err); | |
| 582 | + | console.warn( | |
| 583 | + | `[dumber-parent] Failed to parse OAuth callback data:`, | |
| 584 | + | err, | |
| 585 | + | ); | |
| 519 | 586 | localStorage.removeItem(key); // Clean up invalid data | |
| 520 | 587 | } | |
| 521 | 588 | } | |
| 522 | 589 | } | |
| 523 | 590 | } catch (err) { | |
| 524 | - | console.warn(`[dumber-parent] Failed to poll for popup messages:`, err); | |
| 591 | + | console.warn( | |
| 592 | + | `[dumber-parent] Failed to poll for popup messages:`, | |
| 593 | + | err, | |
| 594 | + | ); | |
| 525 | 595 | } | |
| 526 | 596 | }; | |
| 527 | 597 | ||
| @@ -562,20 +632,19 @@ function detectWindowType(features?: string | null): string { | |||
| 562 | 632 | const url = window.location.href.toLowerCase(); | |
| 563 | 633 | ||
| 564 | 634 | // Check for OAuth callback patterns - be more specific to avoid false positives | |
| 565 | - | const isCallback = ( | |
| 635 | + | const isCallback = | |
| 566 | 636 | // OAuth callback URLs typically have these parameters | |
| 567 | - | url.includes('code=') || | |
| 568 | - | url.includes('access_token=') || | |
| 569 | - | url.includes('id_token=') || | |
| 637 | + | url.includes("code=") || | |
| 638 | + | url.includes("access_token=") || | |
| 639 | + | url.includes("id_token=") || | |
| 570 | 640 | // OAuth error responses | |
| 571 | - | url.includes('error=access_denied') || | |
| 572 | - | url.includes('error=unauthorized') || | |
| 641 | + | url.includes("error=access_denied") || | |
| 642 | + | url.includes("error=unauthorized") || | |
| 573 | 643 | // OAuth callback paths (more specific than just "callback" or "redirect") | |
| 574 | - | url.includes('/oauth/callback') || | |
| 575 | - | url.includes('/auth/callback') || | |
| 576 | - | url.includes('oauth2callback') || | |
| 577 | - | url.includes('googlepopupcallback') | |
| 578 | - | ); | |
| 644 | + | url.includes("/oauth/callback") || | |
| 645 | + | url.includes("/auth/callback") || | |
| 646 | + | url.includes("oauth2callback") || | |
| 647 | + | url.includes("googlepopupcallback"); | |
| 579 | 648 | ||
| 580 | 649 | if (isCallback) { | |
| 581 | 650 | try { | |
| @@ -593,13 +662,18 @@ function detectWindowType(features?: string | null): string { | |||
| 593 | 662 | const age = Date.now() - (mapping.timestamp || 0); | |
| 594 | 663 | if (age < 60000 && mapping.popupId) { | |
| 595 | 664 | targetWebViewId = mapping.popupId; | |
| 596 | - | console.log(`[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`); | |
| 665 | + | console.log( | |
| 666 | + | `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`, | |
| 667 | + | ); | |
| 597 | 668 | ||
| 598 | 669 | // Clean up the mapping since we're using it | |
| 599 | 670 | localStorage.removeItem(popupMappingKey); | |
| 600 | 671 | } | |
| 601 | 672 | } catch (err) { | |
| 602 | - | console.warn(`[oauth-callback] Failed to parse popup mapping:`, err); | |
| 673 | + | console.warn( | |
| 674 | + | `[oauth-callback] Failed to parse popup mapping:`, | |
| 675 | + | err, | |
| 676 | + | ); | |
| 603 | 677 | } | |
| 604 | 678 | } | |
| 605 | 679 | ||
| @@ -610,20 +684,34 @@ function detectWindowType(features?: string | null): string { | |||
| 610 | 684 | isOAuthCallback: true, | |
| 611 | 685 | }; | |
| 612 | 686 | ||
| 613 | - | localStorage.setItem(`oauth_callback_${targetWebViewId}`, JSON.stringify(callbackData)); | |
| 614 | - | console.log(`[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`, callbackData); | |
| 687 | + | localStorage.setItem( | |
| 688 | + | `oauth_callback_${targetWebViewId}`, | |
| 689 | + | JSON.stringify(callbackData), | |
| 690 | + | ); | |
| 691 | + | console.log( | |
| 692 | + | `[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`, | |
| 693 | + | callbackData, | |
| 694 | + | ); | |
| 615 | 695 | ||
| 616 | 696 | // Auto-cleanup sensitive OAuth data after 10 seconds | |
| 617 | 697 | setTimeout(() => { | |
| 618 | 698 | try { | |
| 619 | 699 | localStorage.removeItem(`oauth_callback_${targetWebViewId}`); | |
| 620 | - | console.log(`[oauth-callback] Cleaned up OAuth callback data from localStorage`); | |
| 700 | + | console.log( | |
| 701 | + | `[oauth-callback] Cleaned up OAuth callback data from localStorage`, | |
| 702 | + | ); | |
| 621 | 703 | } catch (err) { | |
| 622 | - | console.warn(`[oauth-callback] Failed to cleanup OAuth callback data:`, err); | |
| 704 | + | console.warn( | |
| 705 | + | `[oauth-callback] Failed to cleanup OAuth callback data:`, | |
| 706 | + | err, | |
| 707 | + | ); | |
| 623 | 708 | } | |
| 624 | 709 | }, 10000); | |
| 625 | 710 | } catch (err) { | |
| 626 | - | console.warn(`[oauth-callback] Failed to store OAuth callback data:`, err); | |
| 711 | + | console.warn( | |
| 712 | + | `[oauth-callback] Failed to store OAuth callback data:`, | |
| 713 | + | err, | |
| 714 | + | ); | |
| 627 | 715 | } | |
| 628 | 716 | } | |
| 629 | 717 | }; | |
| @@ -647,11 +735,11 @@ function detectWindowType(features?: string | null): string { | |||
| 647 | 735 | }); | |
| 648 | 736 | ||
| 649 | 737 | // Also check on navigation events | |
| 650 | - | window.addEventListener('popstate', detectOAuthCallback); | |
| 651 | - | window.addEventListener('hashchange', detectOAuthCallback); | |
| 738 | + | window.addEventListener("popstate", detectOAuthCallback); | |
| 739 | + | window.addEventListener("hashchange", detectOAuthCallback); | |
| 652 | 740 | cleanupHandlers.push(() => { | |
| 653 | - | window.removeEventListener('popstate', detectOAuthCallback); | |
| 654 | - | window.removeEventListener('hashchange', detectOAuthCallback); | |
| 741 | + | window.removeEventListener("popstate", detectOAuthCallback); | |
| 742 | + | window.removeEventListener("hashchange", detectOAuthCallback); | |
| 655 | 743 | }); | |
| 656 | 744 | }; | |
| 657 | 745 | ||
| @@ -737,23 +825,32 @@ function detectWindowType(features?: string | null): string { | |||
| 737 | 825 | return timeSinceInteraction <= INTERACTION_TIMEOUT; | |
| 738 | 826 | }; | |
| 739 | 827 | ||
| 740 | - | ||
| 741 | 828 | const createFakeWindow = (url: string, popupId?: string): WindowProxy => { | |
| 742 | 829 | // Generate unique popup ID if not provided | |
| 743 | - | const actualPopupId = popupId || `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; | |
| 830 | + | const actualPopupId = | |
| 831 | + | popupId || | |
| 832 | + | `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; | |
| 744 | 833 | ||
| 745 | 834 | // Set up communication channel via shared localStorage | |
| 746 | 835 | try { | |
| 747 | - | localStorage.setItem(`popup_${actualPopupId}_parent_info`, JSON.stringify({ | |
| 748 | - | parentUrl: window.location.href, | |
| 749 | - | parentWebViewId: window.__dumber_webview_id, | |
| 750 | - | timestamp: Date.now(), | |
| 751 | - | popupUrl: url, | |
| 752 | - | })); | |
| 836 | + | localStorage.setItem( | |
| 837 | + | `popup_${actualPopupId}_parent_info`, | |
| 838 | + | JSON.stringify({ | |
| 839 | + | parentUrl: window.location.href, | |
| 840 | + | parentWebViewId: window.__dumber_webview_id, | |
| 841 | + | timestamp: Date.now(), | |
| 842 | + | popupUrl: url, | |
| 843 | + | }), | |
| 844 | + | ); | |
| 753 | 845 | ||
| 754 | - | console.log(`[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`); | |
| 846 | + | console.log( | |
| 847 | + | `[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`, | |
| 848 | + | ); | |
| 755 | 849 | } catch (err) { | |
| 756 | - | console.warn(`[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`, err); | |
| 850 | + | console.warn( | |
| 851 | + | `[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`, | |
| 852 | + | err, | |
| 853 | + | ); | |
| 757 | 854 | } | |
| 758 | 855 | ||
| 759 | 856 | return { | |
| @@ -764,33 +861,45 @@ function detectWindowType(features?: string | null): string { | |||
| 764 | 861 | }, | |
| 765 | 862 | focus: () => { | |
| 766 | 863 | try { | |
| 767 | - | localStorage.setItem(`popup_${actualPopupId}_parent_action`, JSON.stringify({ | |
| 768 | - | action: 'focus', | |
| 769 | - | timestamp: Date.now(), | |
| 770 | - | })); | |
| 864 | + | localStorage.setItem( | |
| 865 | + | `popup_${actualPopupId}_parent_action`, | |
| 866 | + | JSON.stringify({ | |
| 867 | + | action: "focus", | |
| 868 | + | timestamp: Date.now(), | |
| 869 | + | }), | |
| 870 | + | ); | |
| 771 | 871 | } catch (err) { | |
| 772 | 872 | console.warn(`[window-open] Failed to store focus action:`, err); | |
| 773 | 873 | } | |
| 774 | 874 | }, | |
| 775 | 875 | blur: () => { | |
| 776 | 876 | try { | |
| 777 | - | localStorage.setItem(`popup_${actualPopupId}_parent_action`, JSON.stringify({ | |
| 778 | - | action: 'blur', | |
| 779 | - | timestamp: Date.now(), | |
| 780 | - | })); | |
| 877 | + | localStorage.setItem( | |
| 878 | + | `popup_${actualPopupId}_parent_action`, | |
| 879 | + | JSON.stringify({ | |
| 880 | + | action: "blur", | |
| 881 | + | timestamp: Date.now(), | |
| 882 | + | }), | |
| 883 | + | ); | |
| 781 | 884 | } catch (err) { | |
| 782 | 885 | console.warn(`[window-open] Failed to store blur action:`, err); | |
| 783 | 886 | } | |
| 784 | 887 | }, | |
| 785 | 888 | postMessage: (data: unknown, targetOrigin?: string) => { | |
| 786 | 889 | try { | |
| 787 | - | localStorage.setItem(`popup_${actualPopupId}_message_to_popup`, JSON.stringify({ | |
| 890 | + | localStorage.setItem( | |
| 891 | + | `popup_${actualPopupId}_message_to_popup`, | |
| 892 | + | JSON.stringify({ | |
| 893 | + | data, | |
| 894 | + | origin: targetOrigin || "*", | |
| 895 | + | timestamp: Date.now(), | |
| 896 | + | source: "parent", | |
| 897 | + | }), | |
| 898 | + | ); | |
| 899 | + | console.log( | |
| 900 | + | `[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`, | |
| 788 | 901 | data, | |
| 789 | - | origin: targetOrigin || '*', | |
| 790 | - | timestamp: Date.now(), | |
| 791 | - | source: 'parent', | |
| 792 | - | })); | |
| 793 | - | console.log(`[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`, data); | |
| 902 | + | ); | |
| 794 | 903 | } catch (err) { | |
| 795 | 904 | console.warn(`[window-open] Failed to store postMessage:`, err); | |
| 796 | 905 | } | |
| @@ -798,9 +907,13 @@ function detectWindowType(features?: string | null): string { | |||
| 798 | 907 | } as unknown as WindowProxy; | |
| 799 | 908 | }; | |
| 800 | 909 | ||
| 801 | - | ||
| 802 | 910 | // Track user interactions for popup validation | |
| 803 | - | const interactionEvents = ["click", "mousedown", "keydown", "touchstart"] as const; | |
| 911 | + | const interactionEvents = [ | |
| 912 | + | "click", | |
| 913 | + | "mousedown", | |
| 914 | + | "keydown", | |
| 915 | + | "touchstart", | |
| 916 | + | ] as const; | |
| 804 | 917 | interactionEvents.forEach((eventType) => { | |
| 805 | 918 | document.addEventListener(eventType, trackUserInteraction, true); | |
| 806 | 919 | }); | |
| @@ -814,7 +927,6 @@ function detectWindowType(features?: string | null): string { | |||
| 814 | 927 | `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`, | |
| 815 | 928 | ); | |
| 816 | 929 | ||
| 817 | - | ||
| 818 | 930 | // Window.open interceptor - only install once | |
| 819 | 931 | if (!window.__dumber_window_open_intercepted) { | |
| 820 | 932 | try { | |
| @@ -837,7 +949,6 @@ function detectWindowType(features?: string | null): string { | |||
| 837 | 949 | features, | |
| 838 | 950 | ); | |
| 839 | 951 | ||
| 840 | - | ||
| 841 | 952 | // Enhanced duplicate check | |
| 842 | 953 | if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) { | |
| 843 | 954 | console.log( | |
| @@ -927,7 +1038,7 @@ function detectWindowType(features?: string | null): string { | |||
| 927 | 1038 | try { | |
| 928 | 1039 | cleanup(); | |
| 929 | 1040 | } catch (cleanupErr) { | |
| 930 | - | console.warn('[dumber] Cleanup handler failed', cleanupErr); | |
| 1041 | + | console.warn("[dumber] Cleanup handler failed", cleanupErr); | |
| 931 | 1042 | } | |
| 932 | 1043 | } | |
| 933 | 1044 | ||
| @@ -942,18 +1053,20 @@ function detectWindowType(features?: string | null): string { | |||
| 942 | 1053 | window.__dumber_showZoomToast = undefined; | |
| 943 | 1054 | window.__dumber_applyDomZoom = undefined; | |
| 944 | 1055 | ||
| 945 | - | if (window.__dumber) { | |
| 946 | - | delete window.__dumber.omnibox; | |
| 947 | - | delete window.__dumber.toast; | |
| 1056 | + | const globalState = window.__dumber; | |
| 1057 | + | if (globalState) { | |
| 1058 | + | const mutableState = globalState as unknown as Record<string, unknown>; | |
| 1059 | + | delete mutableState.omnibox; | |
| 1060 | + | delete mutableState.toast; | |
| 948 | 1061 | } | |
| 949 | 1062 | }; | |
| 950 | 1063 | ||
| 951 | 1064 | window.__dumber_teardown = teardown; | |
| 952 | - | window.addEventListener('pagehide', teardown, { once: true }); | |
| 953 | - | window.addEventListener('beforeunload', teardown, { once: true }); | |
| 1065 | + | window.addEventListener("pagehide", teardown, { once: true }); | |
| 1066 | + | window.addEventListener("beforeunload", teardown, { once: true }); | |
| 954 | 1067 | cleanupHandlers.push(() => { | |
| 955 | - | window.removeEventListener('pagehide', teardown); | |
| 956 | - | window.removeEventListener('beforeunload', teardown); | |
| 1068 | + | window.removeEventListener("pagehide", teardown); | |
| 1069 | + | window.removeEventListener("beforeunload", teardown); | |
| 957 | 1070 | window.__dumber_teardown = undefined; | |
| 958 | 1071 | }); | |
| 959 | 1072 | ||
| @@ -84,7 +84,12 @@ function detectWindowType(features?: string | null): string { | |||||
| 84 | const hasNoLocation = /location=0|location=no/.test(featuresStr); | 84 | const hasNoLocation = /location=0|location=no/.test(featuresStr); | ||
| 85 | 85 | ||||
| 86 | // OAuth/login popups typically have size constraints and disabled UI elements | 86 | // OAuth/login popups typically have size constraints and disabled UI elements | ||
| 87 | - | const popupIndicators = [hasSize, hasNoToolbar, hasNoMenubar, hasNoLocation].filter(Boolean).length; | 87 | + | const popupIndicators = [ |
| 88 | + | hasSize, | |||
| 89 | + | hasNoToolbar, | |||
| 90 | + | hasNoMenubar, | |||
| 91 | + | hasNoLocation, | |||
| 92 | + | ].filter(Boolean).length; | |||
| 88 | 93 | ||||
| 89 | // If 2 or more popup indicators are present, treat as popup | 94 | // If 2 or more popup indicators are present, treat as popup | ||
| 90 | if (popupIndicators >= 2) { | 95 | if (popupIndicators >= 2) { | ||
| @@ -302,14 +307,19 @@ function detectWindowType(features?: string | null): string { | |||||
| 302 | const findParentPopupId = () => { | 307 | const findParentPopupId = () => { | ||
| 303 | const keys = Object.keys(localStorage); | 308 | const keys = Object.keys(localStorage); | ||
| 304 | for (const key of keys) { | 309 | for (const key of keys) { | ||
| 305 | - | if (key.startsWith('popup_') && key.endsWith('_parent_info')) { | 310 | + | if (key.startsWith("popup_") && key.endsWith("_parent_info")) { |
| 306 | - | const popupId = key.replace('popup_', '').replace('_parent_info', ''); | 311 | + | const popupId = key |
| 307 | - | const parentInfo = JSON.parse(localStorage.getItem(key) || '{}'); | 312 | + | .replace("popup_", "") |
| 313 | + | .replace("_parent_info", ""); | |||
| 314 | + | const parentInfo = JSON.parse(localStorage.getItem(key) || "{}"); | |||
| 308 | 315 | ||||
| 309 | // Check if this popup info is recent (within last 30 seconds) | 316 | // Check if this popup info is recent (within last 30 seconds) | ||
| 310 | const age = Date.now() - (parentInfo.timestamp || 0); | 317 | const age = Date.now() - (parentInfo.timestamp || 0); | ||
| 311 | if (age < 30000) { | 318 | if (age < 30000) { | ||
| 312 | - | console.log(`[dumber-popup] Found parent info for popup ID: ${popupId}`, parentInfo); | 319 | + | console.log( |
| 320 | + | `[dumber-popup] Found parent info for popup ID: ${popupId}`, | |||
| 321 | + | parentInfo, | |||
| 322 | + | ); | |||
| 313 | return { popupId, parentInfo }; | 323 | return { popupId, parentInfo }; | ||
| 314 | } | 324 | } | ||
| 315 | } | 325 | } | ||
| @@ -321,55 +331,78 @@ function detectWindowType(features?: string | null): string { | |||||
| 321 | if (parentData && !window.opener) { | 331 | if (parentData && !window.opener) { | ||
| 322 | const { popupId, parentInfo } = parentData; | 332 | const { popupId, parentInfo } = parentData; | ||
| 323 | 333 | ||||
| 324 | - | console.log(`[dumber-popup] Setting up window.opener bridge for popup ID: ${popupId}`); | 334 | + | console.log( |
| 335 | + | `[dumber-popup] Setting up window.opener bridge for popup ID: ${popupId}`, | |||
| 336 | + | ); | |||
| 325 | 337 | ||||
| 326 | // Create window.opener proxy that uses localStorage for communication | 338 | // Create window.opener proxy that uses localStorage for communication | ||
| 327 | window.opener = { | 339 | window.opener = { | ||
| 328 | postMessage: (data: unknown, targetOrigin?: string) => { | 340 | postMessage: (data: unknown, targetOrigin?: string) => { | ||
| 329 | try { | 341 | try { | ||
| 330 | - | localStorage.setItem(`popup_${popupId}_message_to_parent`, JSON.stringify({ | 342 | + | localStorage.setItem( |
| 343 | + | `popup_${popupId}_message_to_parent`, | |||
| 344 | + | JSON.stringify({ | |||
| 345 | + | data, | |||
| 346 | + | origin: targetOrigin || "*", | |||
| 347 | + | timestamp: Date.now(), | |||
| 348 | + | source: "popup", | |||
| 349 | + | }), | |||
| 350 | + | ); | |||
| 351 | + | console.log( | |||
| 352 | + | `[dumber-popup] Sent message to parent via localStorage:`, | |||
| 331 | data, | 353 | data, | ||
| 332 | - | origin: targetOrigin || '*', | 354 | + | ); |
| 333 | - | timestamp: Date.now(), | |||
| 334 | - | source: 'popup', | |||
| 335 | - | })); | |||
| 336 | - | console.log(`[dumber-popup] Sent message to parent via localStorage:`, data); | |||
| 337 | } catch (err) { | 355 | } catch (err) { | ||
| 338 | - | console.warn(`[dumber-popup] Failed to send message to parent:`, err); | 356 | + | console.warn( |
| 357 | + | `[dumber-popup] Failed to send message to parent:`, | |||
| 358 | + | err, | |||
| 359 | + | ); | |||
| 339 | } | 360 | } | ||
| 340 | }, | 361 | }, | ||
| 341 | 362 | ||||
| 342 | focus: () => { | 363 | focus: () => { | ||
| 343 | try { | 364 | try { | ||
| 344 | - | localStorage.setItem(`popup_${popupId}_parent_action`, JSON.stringify({ | 365 | + | localStorage.setItem( |
| 345 | - | action: 'focus', | 366 | + | `popup_${popupId}_parent_action`, |
| 346 | - | timestamp: Date.now(), | 367 | + | JSON.stringify({ |
| 347 | - | })); | 368 | + | action: "focus", |
| 369 | + | timestamp: Date.now(), | |||
| 370 | + | }), | |||
| 371 | + | ); | |||
| 348 | } catch (err) { | 372 | } catch (err) { | ||
| 349 | - | console.warn(`[dumber-popup] Failed to request parent focus:`, err); | 373 | + | console.warn( |
| 374 | + | `[dumber-popup] Failed to request parent focus:`, | |||
| 375 | + | err, | |||
| 376 | + | ); | |||
| 350 | } | 377 | } | ||
| 351 | }, | 378 | }, | ||
| 352 | 379 | ||||
| 353 | blur: () => { | 380 | blur: () => { | ||
| 354 | try { | 381 | try { | ||
| 355 | - | localStorage.setItem(`popup_${popupId}_parent_action`, JSON.stringify({ | 382 | + | localStorage.setItem( |
| 356 | - | action: 'blur', | 383 | + | `popup_${popupId}_parent_action`, |
| 357 | - | timestamp: Date.now(), | 384 | + | JSON.stringify({ |
| 358 | - | })); | 385 | + | action: "blur", |
| 386 | + | timestamp: Date.now(), | |||
| 387 | + | }), | |||
| 388 | + | ); | |||
| 359 | } catch (err) { | 389 | } catch (err) { | ||
| 360 | - | console.warn(`[dumber-popup] Failed to request parent blur:`, err); | 390 | + | console.warn( |
| 391 | + | `[dumber-popup] Failed to request parent blur:`, | |||
| 392 | + | err, | |||
| 393 | + | ); | |||
| 361 | } | 394 | } | ||
| 362 | }, | 395 | }, | ||
| 363 | 396 | ||||
| 364 | location: { | 397 | location: { | ||
| 365 | - | href: parentInfo.parentUrl || '', | 398 | + | href: parentInfo.parentUrl || "", |
| 366 | - | origin: new URL(parentInfo.parentUrl || 'about:blank').origin, | 399 | + | origin: new URL(parentInfo.parentUrl || "about:blank").origin, |
| 367 | }, | 400 | }, | ||
| 368 | 401 | ||||
| 369 | closed: false, | 402 | closed: false, | ||
| 370 | 403 | ||||
| 371 | // Support for custom properties/methods that websites might use | 404 | // Support for custom properties/methods that websites might use | ||
| 372 | - | [Symbol.for('popup.bridge')]: true, | 405 | + | [Symbol.for("popup.bridge")]: true, |
| 373 | } as unknown as Window; | 406 | } as unknown as Window; | ||
| 374 | 407 | ||||
| 375 | // Set up message polling to receive messages from parent | 408 | // Set up message polling to receive messages from parent | ||
| @@ -383,12 +416,15 @@ function detectWindowType(features?: string | null): string { | |||||
| 383 | 416 | ||||
| 384 | // Check if message is recent (within last 5 seconds) | 417 | // Check if message is recent (within last 5 seconds) | ||
| 385 | if (Date.now() - timestamp < 5000) { | 418 | if (Date.now() - timestamp < 5000) { | ||
| 386 | - | console.log(`[dumber-popup] Received message from parent:`, data); | 419 | + | console.log( |
| 420 | + | `[dumber-popup] Received message from parent:`, | |||
| 421 | + | data, | |||
| 422 | + | ); | |||
| 387 | 423 | ||||
| 388 | // Dispatch as MessageEvent to window | 424 | // Dispatch as MessageEvent to window | ||
| 389 | - | const event = new MessageEvent('message', { | 425 | + | const event = new MessageEvent("message", { |
| 390 | data, | 426 | data, | ||
| 391 | - | origin: origin || parentInfo.parentUrl || '', | 427 | + | origin: origin || parentInfo.parentUrl || "", |
| 392 | source: window.opener, | 428 | source: window.opener, | ||
| 393 | }); | 429 | }); | ||
| 394 | window.dispatchEvent(event); | 430 | window.dispatchEvent(event); | ||
| @@ -398,7 +434,10 @@ function detectWindowType(features?: string | null): string { | |||||
| 398 | localStorage.removeItem(messageKey); | 434 | localStorage.removeItem(messageKey); | ||
| 399 | } | 435 | } | ||
| 400 | } catch (err) { | 436 | } catch (err) { | ||
| 401 | - | console.warn(`[dumber-popup] Failed to poll for parent messages:`, err); | 437 | + | console.warn( |
| 438 | + | `[dumber-popup] Failed to poll for parent messages:`, | |||
| 439 | + | err, | |||
| 440 | + | ); | |||
| 402 | } | 441 | } | ||
| 403 | }; | 442 | }; | ||
| 404 | 443 | ||||
| @@ -411,10 +450,15 @@ function detectWindowType(features?: string | null): string { | |||||
| 411 | } | 450 | } | ||
| 412 | }); | 451 | }); | ||
| 413 | 452 | ||||
| 414 | - | console.log(`[dumber-popup] window.opener bridge established successfully`); | 453 | + | console.log( |
| 454 | + | `[dumber-popup] window.opener bridge established successfully`, | |||
| 455 | + | ); | |||
| 415 | } | 456 | } | ||
| 416 | } catch (err) { | 457 | } catch (err) { | ||
| 417 | - | console.warn(`[dumber-popup] Failed to setup window.opener bridge:`, err); | 458 | + | console.warn( |
| 459 | + | `[dumber-popup] Failed to setup window.opener bridge:`, | |||
| 460 | + | err, | |||
| 461 | + | ); | |||
| 418 | } | 462 | } | ||
| 419 | }; | 463 | }; | ||
| 420 | 464 | ||||
| @@ -430,10 +474,11 @@ function detectWindowType(features?: string | null): string { | |||||
| 430 | const hasRelevantData = () => { | 474 | const hasRelevantData = () => { | ||
| 431 | try { | 475 | try { | ||
| 432 | const keys = Object.keys(localStorage); | 476 | const keys = Object.keys(localStorage); | ||
| 433 | - | return keys.some(key => | 477 | + | return keys.some( |
| 434 | - | key.startsWith('popup_mapping_') || | 478 | + | (key) => |
| 435 | - | key.startsWith('oauth_callback_') || | 479 | + | key.startsWith("popup_mapping_") || |
| 436 | - | key.includes('message_to_parent') | 480 | + | key.startsWith("oauth_callback_") || |
| 481 | + | key.includes("message_to_parent"), | |||
| 437 | ); | 482 | ); | ||
| 438 | } catch { | 483 | } catch { | ||
| 439 | return false; | 484 | return false; | ||
| @@ -451,19 +496,23 @@ function detectWindowType(features?: string | null): string { | |||||
| 451 | 496 | ||||
| 452 | for (const key of keys) { | 497 | for (const key of keys) { | ||
| 453 | // Handle popup messages to parent | 498 | // Handle popup messages to parent | ||
| 454 | - | if (key.includes('message_to_parent')) { | 499 | + | if (key.includes("message_to_parent")) { |
| 455 | const messageData = localStorage.getItem(key); | 500 | const messageData = localStorage.getItem(key); | ||
| 456 | if (!messageData) continue; | 501 | if (!messageData) continue; | ||
| 457 | 502 | ||||
| 458 | try { | 503 | try { | ||
| 459 | - | const { data, origin, timestamp, source } = JSON.parse(messageData); | 504 | + | const { data, origin, timestamp, source } = |
| 505 | + | JSON.parse(messageData); | |||
| 460 | 506 | ||||
| 461 | // Check if message is recent (within last 5 seconds) and from a popup | 507 | // Check if message is recent (within last 5 seconds) and from a popup | ||
| 462 | - | if (Date.now() - timestamp < 5000 && source === 'popup') { | 508 | + | if (Date.now() - timestamp < 5000 && source === "popup") { |
| 463 | - | console.log(`[dumber-parent] Received message from popup:`, data); | 509 | + | console.log( |
| 510 | + | `[dumber-parent] Received message from popup:`, | |||
| 511 | + | data, | |||
| 512 | + | ); | |||
| 464 | 513 | ||||
| 465 | // Dispatch as MessageEvent to parent window | 514 | // Dispatch as MessageEvent to parent window | ||
| 466 | - | const event = new MessageEvent('message', { | 515 | + | const event = new MessageEvent("message", { |
| 467 | data, | 516 | data, | ||
| 468 | origin: origin || window.location.origin, | 517 | origin: origin || window.location.origin, | ||
| 469 | source: null, // popup reference would be here in real browser | 518 | source: null, // popup reference would be here in real browser | ||
| @@ -474,54 +523,75 @@ function detectWindowType(features?: string | null): string { | |||||
| 474 | localStorage.removeItem(key); | 523 | localStorage.removeItem(key); | ||
| 475 | } | 524 | } | ||
| 476 | } catch (err) { | 525 | } catch (err) { | ||
| 477 | - | console.warn(`[dumber-parent] Failed to parse popup message:`, err); | 526 | + | console.warn( |
| 527 | + | `[dumber-parent] Failed to parse popup message:`, | |||
| 528 | + | err, | |||
| 529 | + | ); | |||
| 478 | localStorage.removeItem(key); // Clean up invalid message | 530 | localStorage.removeItem(key); // Clean up invalid message | ||
| 479 | } | 531 | } | ||
| 480 | } | 532 | } | ||
| 481 | 533 | ||||
| 482 | // Handle OAuth callback detection from popups | 534 | // Handle OAuth callback detection from popups | ||
| 483 | - | if (key.startsWith('oauth_callback_')) { | 535 | + | if (key.startsWith("oauth_callback_")) { |
| 484 | const callbackData = localStorage.getItem(key); | 536 | const callbackData = localStorage.getItem(key); | ||
| 485 | if (!callbackData) continue; | 537 | if (!callbackData) continue; | ||
| 486 | 538 | ||||
| 487 | try { | 539 | try { | ||
| 488 | - | const { url, webviewId, timestamp, isOAuthCallback } = JSON.parse(callbackData); | 540 | + | const { url, webviewId, timestamp, isOAuthCallback } = |
| 541 | + | JSON.parse(callbackData); | |||
| 489 | 542 | ||||
| 490 | // Check if callback is recent (within last 10 seconds) and is an OAuth callback | 543 | // Check if callback is recent (within last 10 seconds) and is an OAuth callback | ||
| 491 | if (Date.now() - timestamp < 10000 && isOAuthCallback) { | 544 | if (Date.now() - timestamp < 10000 && isOAuthCallback) { | ||
| 492 | - | console.log(`[dumber-parent] OAuth callback detected for webview ${webviewId}:`, url); | 545 | + | console.log( |
| 546 | + | `[dumber-parent] OAuth callback detected for webview ${webviewId}:`, | |||
| 547 | + | url, | |||
| 548 | + | ); | |||
| 493 | 549 | ||||
| 494 | // Send close request to backend for this popup webview | 550 | // Send close request to backend for this popup webview | ||
| 495 | const bridge = window.webkit?.messageHandlers?.dumber; | 551 | const bridge = window.webkit?.messageHandlers?.dumber; | ||
| 496 | - | if (bridge && typeof bridge.postMessage === 'function') { | 552 | + | if (bridge && typeof bridge.postMessage === "function") { |
| 497 | try { | 553 | try { | ||
| 498 | const closeMessage = { | 554 | const closeMessage = { | ||
| 499 | - | type: 'close-popup', | 555 | + | type: "close-popup", |
| 500 | webviewId, | 556 | webviewId, | ||
| 501 | - | reason: 'oauth-callback-success', | 557 | + | reason: "oauth-callback-success", |
| 502 | - | timestamp: Date.now() | 558 | + | timestamp: Date.now(), |
| 503 | }; | 559 | }; | ||
| 504 | 560 | ||||
| 505 | - | console.log(`[dumber-parent] Sending popup close request:`, closeMessage); | 561 | + | console.log( |
| 562 | + | `[dumber-parent] Sending popup close request:`, | |||
| 563 | + | closeMessage, | |||
| 564 | + | ); | |||
| 506 | bridge.postMessage(JSON.stringify(closeMessage)); | 565 | bridge.postMessage(JSON.stringify(closeMessage)); | ||
| 507 | } catch (err) { | 566 | } catch (err) { | ||
| 508 | - | console.warn(`[dumber-parent] Failed to send popup close request:`, err); | 567 | + | console.warn( |
| 568 | + | `[dumber-parent] Failed to send popup close request:`, | |||
| 569 | + | err, | |||
| 570 | + | ); | |||
| 509 | } | 571 | } | ||
| 510 | } else { | 572 | } else { | ||
| 511 | - | console.warn(`[dumber-parent] No webkit bridge available for popup close request`); | 573 | + | console.warn( |
| 574 | + | `[dumber-parent] No webkit bridge available for popup close request`, | |||
| 575 | + | ); | |||
| 512 | } | 576 | } | ||
| 513 | 577 | ||||
| 514 | // Clean up OAuth callback data | 578 | // Clean up OAuth callback data | ||
| 515 | localStorage.removeItem(key); | 579 | localStorage.removeItem(key); | ||
| 516 | } | 580 | } | ||
| 517 | } catch (err) { | 581 | } catch (err) { | ||
| 518 | - | console.warn(`[dumber-parent] Failed to parse OAuth callback data:`, err); | 582 | + | console.warn( |
| 583 | + | `[dumber-parent] Failed to parse OAuth callback data:`, | |||
| 584 | + | err, | |||
| 585 | + | ); | |||
| 519 | localStorage.removeItem(key); // Clean up invalid data | 586 | localStorage.removeItem(key); // Clean up invalid data | ||
| 520 | } | 587 | } | ||
| 521 | } | 588 | } | ||
| 522 | } | 589 | } | ||
| 523 | } catch (err) { | 590 | } catch (err) { | ||
| 524 | - | console.warn(`[dumber-parent] Failed to poll for popup messages:`, err); | 591 | + | console.warn( |
| 592 | + | `[dumber-parent] Failed to poll for popup messages:`, | |||
| 593 | + | err, | |||
| 594 | + | ); | |||
| 525 | } | 595 | } | ||
| 526 | }; | 596 | }; | ||
| 527 | 597 | ||||
| @@ -562,20 +632,19 @@ function detectWindowType(features?: string | null): string { | |||||
| 562 | const url = window.location.href.toLowerCase(); | 632 | const url = window.location.href.toLowerCase(); | ||
| 563 | 633 | ||||
| 564 | // Check for OAuth callback patterns - be more specific to avoid false positives | 634 | // Check for OAuth callback patterns - be more specific to avoid false positives | ||
| 565 | - | const isCallback = ( | 635 | + | const isCallback = |
| 566 | // OAuth callback URLs typically have these parameters | 636 | // OAuth callback URLs typically have these parameters | ||
| 567 | - | url.includes('code=') || | 637 | + | url.includes("code=") || |
| 568 | - | url.includes('access_token=') || | 638 | + | url.includes("access_token=") || |
| 569 | - | url.includes('id_token=') || | 639 | + | url.includes("id_token=") || |
| 570 | // OAuth error responses | 640 | // OAuth error responses | ||
| 571 | - | url.includes('error=access_denied') || | 641 | + | url.includes("error=access_denied") || |
| 572 | - | url.includes('error=unauthorized') || | 642 | + | url.includes("error=unauthorized") || |
| 573 | // OAuth callback paths (more specific than just "callback" or "redirect") | 643 | // OAuth callback paths (more specific than just "callback" or "redirect") | ||
| 574 | - | url.includes('/oauth/callback') || | 644 | + | url.includes("/oauth/callback") || |
| 575 | - | url.includes('/auth/callback') || | 645 | + | url.includes("/auth/callback") || |
| 576 | - | url.includes('oauth2callback') || | 646 | + | url.includes("oauth2callback") || |
| 577 | - | url.includes('googlepopupcallback') | 647 | + | url.includes("googlepopupcallback"); |
| 578 | - | ); | |||
| 579 | 648 | ||||
| 580 | if (isCallback) { | 649 | if (isCallback) { | ||
| 581 | try { | 650 | try { | ||
| @@ -593,13 +662,18 @@ function detectWindowType(features?: string | null): string { | |||||
| 593 | const age = Date.now() - (mapping.timestamp || 0); | 662 | const age = Date.now() - (mapping.timestamp || 0); | ||
| 594 | if (age < 60000 && mapping.popupId) { | 663 | if (age < 60000 && mapping.popupId) { | ||
| 595 | targetWebViewId = mapping.popupId; | 664 | targetWebViewId = mapping.popupId; | ||
| 596 | - | console.log(`[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`); | 665 | + | console.log( |
| 666 | + | `[oauth-callback] Parent detected OAuth callback, targeting popup webview: ${targetWebViewId}`, | |||
| 667 | + | ); | |||
| 597 | 668 | ||||
| 598 | // Clean up the mapping since we're using it | 669 | // Clean up the mapping since we're using it | ||
| 599 | localStorage.removeItem(popupMappingKey); | 670 | localStorage.removeItem(popupMappingKey); | ||
| 600 | } | 671 | } | ||
| 601 | } catch (err) { | 672 | } catch (err) { | ||
| 602 | - | console.warn(`[oauth-callback] Failed to parse popup mapping:`, err); | 673 | + | console.warn( |
| 674 | + | `[oauth-callback] Failed to parse popup mapping:`, | |||
| 675 | + | err, | |||
| 676 | + | ); | |||
| 603 | } | 677 | } | ||
| 604 | } | 678 | } | ||
| 605 | 679 | ||||
| @@ -610,20 +684,34 @@ function detectWindowType(features?: string | null): string { | |||||
| 610 | isOAuthCallback: true, | 684 | isOAuthCallback: true, | ||
| 611 | }; | 685 | }; | ||
| 612 | 686 | ||||
| 613 | - | localStorage.setItem(`oauth_callback_${targetWebViewId}`, JSON.stringify(callbackData)); | 687 | + | localStorage.setItem( |
| 614 | - | console.log(`[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`, callbackData); | 688 | + | `oauth_callback_${targetWebViewId}`, |
| 689 | + | JSON.stringify(callbackData), | |||
| 690 | + | ); | |||
| 691 | + | console.log( | |||
| 692 | + | `[oauth-callback] Detected OAuth callback, targeting webview ${targetWebViewId}:`, | |||
| 693 | + | callbackData, | |||
| 694 | + | ); | |||
| 615 | 695 | ||||
| 616 | // Auto-cleanup sensitive OAuth data after 10 seconds | 696 | // Auto-cleanup sensitive OAuth data after 10 seconds | ||
| 617 | setTimeout(() => { | 697 | setTimeout(() => { | ||
| 618 | try { | 698 | try { | ||
| 619 | localStorage.removeItem(`oauth_callback_${targetWebViewId}`); | 699 | localStorage.removeItem(`oauth_callback_${targetWebViewId}`); | ||
| 620 | - | console.log(`[oauth-callback] Cleaned up OAuth callback data from localStorage`); | 700 | + | console.log( |
| 701 | + | `[oauth-callback] Cleaned up OAuth callback data from localStorage`, | |||
| 702 | + | ); | |||
| 621 | } catch (err) { | 703 | } catch (err) { | ||
| 622 | - | console.warn(`[oauth-callback] Failed to cleanup OAuth callback data:`, err); | 704 | + | console.warn( |
| 705 | + | `[oauth-callback] Failed to cleanup OAuth callback data:`, | |||
| 706 | + | err, | |||
| 707 | + | ); | |||
| 623 | } | 708 | } | ||
| 624 | }, 10000); | 709 | }, 10000); | ||
| 625 | } catch (err) { | 710 | } catch (err) { | ||
| 626 | - | console.warn(`[oauth-callback] Failed to store OAuth callback data:`, err); | 711 | + | console.warn( |
| 712 | + | `[oauth-callback] Failed to store OAuth callback data:`, | |||
| 713 | + | err, | |||
| 714 | + | ); | |||
| 627 | } | 715 | } | ||
| 628 | } | 716 | } | ||
| 629 | }; | 717 | }; | ||
| @@ -647,11 +735,11 @@ function detectWindowType(features?: string | null): string { | |||||
| 647 | }); | 735 | }); | ||
| 648 | 736 | ||||
| 649 | // Also check on navigation events | 737 | // Also check on navigation events | ||
| 650 | - | window.addEventListener('popstate', detectOAuthCallback); | 738 | + | window.addEventListener("popstate", detectOAuthCallback); |
| 651 | - | window.addEventListener('hashchange', detectOAuthCallback); | 739 | + | window.addEventListener("hashchange", detectOAuthCallback); |
| 652 | cleanupHandlers.push(() => { | 740 | cleanupHandlers.push(() => { | ||
| 653 | - | window.removeEventListener('popstate', detectOAuthCallback); | 741 | + | window.removeEventListener("popstate", detectOAuthCallback); |
| 654 | - | window.removeEventListener('hashchange', detectOAuthCallback); | 742 | + | window.removeEventListener("hashchange", detectOAuthCallback); |
| 655 | }); | 743 | }); | ||
| 656 | }; | 744 | }; | ||
| 657 | 745 | ||||
| @@ -737,23 +825,32 @@ function detectWindowType(features?: string | null): string { | |||||
| 737 | return timeSinceInteraction <= INTERACTION_TIMEOUT; | 825 | return timeSinceInteraction <= INTERACTION_TIMEOUT; | ||
| 738 | }; | 826 | }; | ||
| 739 | 827 | ||||
| 740 | - | ||||
| 741 | const createFakeWindow = (url: string, popupId?: string): WindowProxy => { | 828 | const createFakeWindow = (url: string, popupId?: string): WindowProxy => { | ||
| 742 | // Generate unique popup ID if not provided | 829 | // Generate unique popup ID if not provided | ||
| 743 | - | const actualPopupId = popupId || `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; | 830 | + | const actualPopupId = |
| 831 | + | popupId || | |||
| 832 | + | `${window.__dumber_webview_id}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; | |||
| 744 | 833 | ||||
| 745 | // Set up communication channel via shared localStorage | 834 | // Set up communication channel via shared localStorage | ||
| 746 | try { | 835 | try { | ||
| 747 | - | localStorage.setItem(`popup_${actualPopupId}_parent_info`, JSON.stringify({ | 836 | + | localStorage.setItem( |
| 748 | - | parentUrl: window.location.href, | 837 | + | `popup_${actualPopupId}_parent_info`, |
| 749 | - | parentWebViewId: window.__dumber_webview_id, | 838 | + | JSON.stringify({ |
| 750 | - | timestamp: Date.now(), | 839 | + | parentUrl: window.location.href, |
| 751 | - | popupUrl: url, | 840 | + | parentWebViewId: window.__dumber_webview_id, |
| 752 | - | })); | 841 | + | timestamp: Date.now(), |
| 842 | + | popupUrl: url, | |||
| 843 | + | }), | |||
| 844 | + | ); | |||
| 753 | 845 | ||||
| 754 | - | console.log(`[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`); | 846 | + | console.log( |
| 847 | + | `[window-open] [WebView ${window.__dumber_webview_id}] Set up shared storage communication for popup ID: ${actualPopupId}`, | |||
| 848 | + | ); | |||
| 755 | } catch (err) { | 849 | } catch (err) { | ||
| 756 | - | console.warn(`[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`, err); | 850 | + | console.warn( |
| 851 | + | `[window-open] [WebView ${window.__dumber_webview_id}] Failed to set up shared storage:`, | |||
| 852 | + | err, | |||
| 853 | + | ); | |||
| 757 | } | 854 | } | ||
| 758 | 855 | ||||
| 759 | return { | 856 | return { | ||
| @@ -764,33 +861,45 @@ function detectWindowType(features?: string | null): string { | |||||
| 764 | }, | 861 | }, | ||
| 765 | focus: () => { | 862 | focus: () => { | ||
| 766 | try { | 863 | try { | ||
| 767 | - | localStorage.setItem(`popup_${actualPopupId}_parent_action`, JSON.stringify({ | 864 | + | localStorage.setItem( |
| 768 | - | action: 'focus', | 865 | + | `popup_${actualPopupId}_parent_action`, |
| 769 | - | timestamp: Date.now(), | 866 | + | JSON.stringify({ |
| 770 | - | })); | 867 | + | action: "focus", |
| 868 | + | timestamp: Date.now(), | |||
| 869 | + | }), | |||
| 870 | + | ); | |||
| 771 | } catch (err) { | 871 | } catch (err) { | ||
| 772 | console.warn(`[window-open] Failed to store focus action:`, err); | 872 | console.warn(`[window-open] Failed to store focus action:`, err); | ||
| 773 | } | 873 | } | ||
| 774 | }, | 874 | }, | ||
| 775 | blur: () => { | 875 | blur: () => { | ||
| 776 | try { | 876 | try { | ||
| 777 | - | localStorage.setItem(`popup_${actualPopupId}_parent_action`, JSON.stringify({ | 877 | + | localStorage.setItem( |
| 778 | - | action: 'blur', | 878 | + | `popup_${actualPopupId}_parent_action`, |
| 779 | - | timestamp: Date.now(), | 879 | + | JSON.stringify({ |
| 780 | - | })); | 880 | + | action: "blur", |
| 881 | + | timestamp: Date.now(), | |||
| 882 | + | }), | |||
| 883 | + | ); | |||
| 781 | } catch (err) { | 884 | } catch (err) { | ||
| 782 | console.warn(`[window-open] Failed to store blur action:`, err); | 885 | console.warn(`[window-open] Failed to store blur action:`, err); | ||
| 783 | } | 886 | } | ||
| 784 | }, | 887 | }, | ||
| 785 | postMessage: (data: unknown, targetOrigin?: string) => { | 888 | postMessage: (data: unknown, targetOrigin?: string) => { | ||
| 786 | try { | 889 | try { | ||
| 787 | - | localStorage.setItem(`popup_${actualPopupId}_message_to_popup`, JSON.stringify({ | 890 | + | localStorage.setItem( |
| 891 | + | `popup_${actualPopupId}_message_to_popup`, | |||
| 892 | + | JSON.stringify({ | |||
| 893 | + | data, | |||
| 894 | + | origin: targetOrigin || "*", | |||
| 895 | + | timestamp: Date.now(), | |||
| 896 | + | source: "parent", | |||
| 897 | + | }), | |||
| 898 | + | ); | |||
| 899 | + | console.log( | |||
| 900 | + | `[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`, | |||
| 788 | data, | 901 | data, | ||
| 789 | - | origin: targetOrigin || '*', | 902 | + | ); |
| 790 | - | timestamp: Date.now(), | |||
| 791 | - | source: 'parent', | |||
| 792 | - | })); | |||
| 793 | - | console.log(`[window-open] [WebView ${window.__dumber_webview_id}] Stored message for popup ${actualPopupId}:`, data); | |||
| 794 | } catch (err) { | 903 | } catch (err) { | ||
| 795 | console.warn(`[window-open] Failed to store postMessage:`, err); | 904 | console.warn(`[window-open] Failed to store postMessage:`, err); | ||
| 796 | } | 905 | } | ||
| @@ -798,9 +907,13 @@ function detectWindowType(features?: string | null): string { | |||||
| 798 | } as unknown as WindowProxy; | 907 | } as unknown as WindowProxy; | ||
| 799 | }; | 908 | }; | ||
| 800 | 909 | ||||
| 801 | - | ||||
| 802 | // Track user interactions for popup validation | 910 | // Track user interactions for popup validation | ||
| 803 | - | const interactionEvents = ["click", "mousedown", "keydown", "touchstart"] as const; | 911 | + | const interactionEvents = [ |
| 912 | + | "click", | |||
| 913 | + | "mousedown", | |||
| 914 | + | "keydown", | |||
| 915 | + | "touchstart", | |||
| 916 | + | ] as const; | |||
| 804 | interactionEvents.forEach((eventType) => { | 917 | interactionEvents.forEach((eventType) => { | ||
| 805 | document.addEventListener(eventType, trackUserInteraction, true); | 918 | document.addEventListener(eventType, trackUserInteraction, true); | ||
| 806 | }); | 919 | }); | ||
| @@ -814,7 +927,6 @@ function detectWindowType(features?: string | null): string { | |||||
| 814 | `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`, | 927 | `[window-open] [WebView ${window.__dumber_webview_id}] User interaction tracking enabled`, | ||
| 815 | ); | 928 | ); | ||
| 816 | 929 | ||||
| 817 | - | ||||
| 818 | // Window.open interceptor - only install once | 930 | // Window.open interceptor - only install once | ||
| 819 | if (!window.__dumber_window_open_intercepted) { | 931 | if (!window.__dumber_window_open_intercepted) { | ||
| 820 | try { | 932 | try { | ||
| @@ -837,7 +949,6 @@ function detectWindowType(features?: string | null): string { | |||||
| 837 | features, | 949 | features, | ||
| 838 | ); | 950 | ); | ||
| 839 | 951 | ||||
| 840 | - | ||||
| 841 | // Enhanced duplicate check | 952 | // Enhanced duplicate check | ||
| 842 | if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) { | 953 | if (windowOpenDebouncer.isDuplicate(urlString, webviewId)) { | ||
| 843 | console.log( | 954 | console.log( | ||
| @@ -927,7 +1038,7 @@ function detectWindowType(features?: string | null): string { | |||||
| 927 | try { | 1038 | try { | ||
| 928 | cleanup(); | 1039 | cleanup(); | ||
| 929 | } catch (cleanupErr) { | 1040 | } catch (cleanupErr) { | ||
| 930 | - | console.warn('[dumber] Cleanup handler failed', cleanupErr); | 1041 | + | console.warn("[dumber] Cleanup handler failed", cleanupErr); |
| 931 | } | 1042 | } | ||
| 932 | } | 1043 | } | ||
| 933 | 1044 | ||||
| @@ -942,18 +1053,20 @@ function detectWindowType(features?: string | null): string { | |||||
| 942 | window.__dumber_showZoomToast = undefined; | 1053 | window.__dumber_showZoomToast = undefined; | ||
| 943 | window.__dumber_applyDomZoom = undefined; | 1054 | window.__dumber_applyDomZoom = undefined; | ||
| 944 | 1055 | ||||
| 945 | - | if (window.__dumber) { | 1056 | + | const globalState = window.__dumber; |
| 946 | - | delete window.__dumber.omnibox; | 1057 | + | if (globalState) { |
| 947 | - | delete window.__dumber.toast; | 1058 | + | const mutableState = globalState as unknown as Record<string, unknown>; |
| 1059 | + | delete mutableState.omnibox; | |||
| 1060 | + | delete mutableState.toast; | |||
| 948 | } | 1061 | } | ||
| 949 | }; | 1062 | }; | ||
| 950 | 1063 | ||||
| 951 | window.__dumber_teardown = teardown; | 1064 | window.__dumber_teardown = teardown; | ||
| 952 | - | window.addEventListener('pagehide', teardown, { once: true }); | 1065 | + | window.addEventListener("pagehide", teardown, { once: true }); |
| 953 | - | window.addEventListener('beforeunload', teardown, { once: true }); | 1066 | + | window.addEventListener("beforeunload", teardown, { once: true }); |
| 954 | cleanupHandlers.push(() => { | 1067 | cleanupHandlers.push(() => { | ||
| 955 | - | window.removeEventListener('pagehide', teardown); | 1068 | + | window.removeEventListener("pagehide", teardown); |
| 956 | - | window.removeEventListener('beforeunload', teardown); | 1069 | + | window.removeEventListener("beforeunload", teardown); |
| 957 | window.__dumber_teardown = undefined; | 1070 | window.__dumber_teardown = undefined; | ||
| 958 | }); | 1071 | }); | ||
| 959 | 1072 | ||||
M gui/src/injected/modules/color-scheme.ts +103 -11
| @@ -22,10 +22,23 @@ interface ColorSchemeConfig { | |||
| 22 | 22 | ||
| 23 | 23 | class ColorSchemeManager { | |
| 24 | 24 | private config: ColorSchemeConfig; | |
| 25 | + | private metaElement: HTMLMetaElement | null = null; | |
| 26 | + | private styleElement: HTMLStyleElement | null = null; | |
| 27 | + | private userPreference: "light" | "dark" | null = null; | |
| 28 | + | private static readonly STORAGE_KEY = "dumber.theme"; | |
| 29 | + | private static instance: ColorSchemeManager | null = null; | |
| 30 | + | private static matchMediaPatched = false; | |
| 31 | + | private static originalMatchMedia: typeof window.matchMedia | null = null; | |
| 25 | 32 | ||
| 26 | 33 | constructor() { | |
| 34 | + | ColorSchemeManager.instance = this; | |
| 35 | + | ||
| 36 | + | this.userPreference = this.getStoredPreference(); | |
| 37 | + | ||
| 27 | 38 | // Use GTK preference if available, otherwise detect from system | |
| 28 | - | const prefersDark = window.__dumber_gtk_prefers_dark ?? false; | |
| 39 | + | const prefersDark = this.userPreference | |
| 40 | + | ? this.userPreference === "dark" | |
| 41 | + | : (window.__dumber_gtk_prefers_dark ?? false); | |
| 29 | 42 | ||
| 30 | 43 | this.config = { | |
| 31 | 44 | isDark: prefersDark, | |
| @@ -79,32 +92,60 @@ class ColorSchemeManager { | |||
| 79 | 92 | } | |
| 80 | 93 | ||
| 81 | 94 | private injectColorSchemeMeta(): void { | |
| 82 | - | const meta = document.createElement("meta"); | |
| 95 | + | if (this.metaElement && this.metaElement.isConnected) { | |
| 96 | + | this.metaElement.content = this.config.metaContent; | |
| 97 | + | return; | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | const existing = document.querySelector( | |
| 101 | + | 'meta[name="color-scheme"]', | |
| 102 | + | ) as HTMLMetaElement | null; | |
| 103 | + | const meta = existing ?? document.createElement("meta"); | |
| 83 | 104 | meta.name = "color-scheme"; | |
| 84 | 105 | meta.content = this.config.metaContent; | |
| 85 | - | document.documentElement.appendChild(meta); | |
| 106 | + | if (!existing) { | |
| 107 | + | document.documentElement.appendChild(meta); | |
| 108 | + | } | |
| 109 | + | this.metaElement = meta; | |
| 86 | 110 | } | |
| 87 | 111 | ||
| 88 | 112 | private applyRootStyles(): void { | |
| 89 | - | const style = document.createElement("style"); | |
| 113 | + | if (this.styleElement && this.styleElement.isConnected) { | |
| 114 | + | this.styleElement.textContent = this.config.rootStyle; | |
| 115 | + | return; | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | const existing = document.querySelector( | |
| 119 | + | "style[data-dumber-color-scheme]", | |
| 120 | + | ) as HTMLStyleElement | null; | |
| 121 | + | const style = existing ?? document.createElement("style"); | |
| 122 | + | style.setAttribute("data-dumber-color-scheme", ""); | |
| 90 | 123 | style.textContent = this.config.rootStyle; | |
| 91 | - | document.documentElement.appendChild(style); | |
| 124 | + | if (!existing) { | |
| 125 | + | document.documentElement.appendChild(style); | |
| 126 | + | } | |
| 127 | + | this.styleElement = style; | |
| 92 | 128 | } | |
| 93 | 129 | ||
| 94 | 130 | private overrideMatchMedia(): void { | |
| 131 | + | if (ColorSchemeManager.matchMediaPatched) { | |
| 132 | + | return; | |
| 133 | + | } | |
| 134 | + | ||
| 95 | 135 | const darkQuery = "(prefers-color-scheme: dark)"; | |
| 96 | 136 | const lightQuery = "(prefers-color-scheme: light)"; | |
| 97 | - | const originalMatchMedia = window.matchMedia; | |
| 137 | + | ColorSchemeManager.originalMatchMedia = window.matchMedia.bind(window); | |
| 98 | 138 | ||
| 99 | 139 | window.matchMedia = (query: string): MediaQueryList => { | |
| 140 | + | const manager = ColorSchemeManager.instance; | |
| 100 | 141 | if ( | |
| 142 | + | manager && | |
| 101 | 143 | typeof query === "string" && | |
| 102 | 144 | (query.includes(darkQuery) || query.includes(lightQuery)) | |
| 103 | 145 | ) { | |
| 104 | - | // Create a mock MediaQueryList that reflects our GTK preference | |
| 105 | 146 | const matches = query.includes("dark") | |
| 106 | - | ? this.config.isDark | |
| 107 | - | : !this.config.isDark; | |
| 147 | + | ? manager.config.isDark | |
| 148 | + | : !manager.config.isDark; | |
| 108 | 149 | ||
| 109 | 150 | return { | |
| 110 | 151 | matches, | |
| @@ -118,9 +159,15 @@ class ColorSchemeManager { | |||
| 118 | 159 | } as MediaQueryList; | |
| 119 | 160 | } | |
| 120 | 161 | ||
| 121 | - | // For non-color-scheme queries, use the original implementation | |
| 122 | - | return originalMatchMedia.call(window, query); | |
| 162 | + | const original = ColorSchemeManager.originalMatchMedia; | |
| 163 | + | if (!original) { | |
| 164 | + | throw new Error("Original matchMedia not available"); | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | return original.call(window, query); | |
| 123 | 168 | }; | |
| 169 | + | ||
| 170 | + | ColorSchemeManager.matchMediaPatched = true; | |
| 124 | 171 | } | |
| 125 | 172 | ||
| 126 | 173 | private setupThemeIntegration(): void { | |
| @@ -160,6 +207,42 @@ class ColorSchemeManager { | |||
| 160 | 207 | // Re-apply all configurations | |
| 161 | 208 | this.initialize(); | |
| 162 | 209 | } | |
| 210 | + | ||
| 211 | + | private getStoredPreference(): "light" | "dark" | null { | |
| 212 | + | try { | |
| 213 | + | const value = localStorage.getItem(ColorSchemeManager.STORAGE_KEY); | |
| 214 | + | if (value === "light" || value === "dark") { | |
| 215 | + | return value; | |
| 216 | + | } | |
| 217 | + | } catch (error) { | |
| 218 | + | console.warn("[dumber] Failed to read stored theme preference", error); | |
| 219 | + | } | |
| 220 | + | return null; | |
| 221 | + | } | |
| 222 | + | ||
| 223 | + | private storePreference(theme: "light" | "dark"): void { | |
| 224 | + | try { | |
| 225 | + | localStorage.setItem(ColorSchemeManager.STORAGE_KEY, theme); | |
| 226 | + | } catch (error) { | |
| 227 | + | console.warn("[dumber] Failed to persist theme preference", error); | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | /** | |
| 232 | + | * Persist and apply an explicit user preference | |
| 233 | + | */ | |
| 234 | + | public setUserPreference(theme: "light" | "dark"): void { | |
| 235 | + | if (this.userPreference === theme && this.config.colorScheme === theme) { | |
| 236 | + | return; | |
| 237 | + | } | |
| 238 | + | this.userPreference = theme; | |
| 239 | + | this.storePreference(theme); | |
| 240 | + | this.updateColorScheme(theme === "dark"); | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | public getCurrentTheme(): "light" | "dark" { | |
| 244 | + | return this.config.colorScheme; | |
| 245 | + | } | |
| 163 | 246 | } | |
| 164 | 247 | ||
| 165 | 248 | // Initialize color scheme management | |
| @@ -169,3 +252,12 @@ const colorSchemeManager = new ColorSchemeManager(); | |||
| 169 | 252 | (window as any).__dumber_color_scheme_manager = colorSchemeManager; | |
| 170 | 253 | ||
| 171 | 254 | export default colorSchemeManager; | |
| 255 | + | ||
| 256 | + | declare global { | |
| 257 | + | interface Window { | |
| 258 | + | __dumber_color_scheme_manager?: { | |
| 259 | + | setUserPreference: (theme: "light" | "dark") => void; | |
| 260 | + | getCurrentTheme: () => "light" | "dark"; | |
| 261 | + | }; | |
| 262 | + | } | |
| 263 | + | } | |
| @@ -22,10 +22,23 @@ interface ColorSchemeConfig { | |||||
| 22 | 22 | ||||
| 23 | class ColorSchemeManager { | 23 | class ColorSchemeManager { | ||
| 24 | private config: ColorSchemeConfig; | 24 | private config: ColorSchemeConfig; | ||
| 25 | + | private metaElement: HTMLMetaElement | null = null; | |||
| 26 | + | private styleElement: HTMLStyleElement | null = null; | |||
| 27 | + | private userPreference: "light" | "dark" | null = null; | |||
| 28 | + | private static readonly STORAGE_KEY = "dumber.theme"; | |||
| 29 | + | private static instance: ColorSchemeManager | null = null; | |||
| 30 | + | private static matchMediaPatched = false; | |||
| 31 | + | private static originalMatchMedia: typeof window.matchMedia | null = null; | |||
| 25 | 32 | ||||
| 26 | constructor() { | 33 | constructor() { | ||
| 34 | + | ColorSchemeManager.instance = this; | |||
| 35 | + | ||||
| 36 | + | this.userPreference = this.getStoredPreference(); | |||
| 37 | + | ||||
| 27 | // Use GTK preference if available, otherwise detect from system | 38 | // Use GTK preference if available, otherwise detect from system | ||
| 28 | - | const prefersDark = window.__dumber_gtk_prefers_dark ?? false; | 39 | + | const prefersDark = this.userPreference |
| 40 | + | ? this.userPreference === "dark" | |||
| 41 | + | : (window.__dumber_gtk_prefers_dark ?? false); | |||
| 29 | 42 | ||||
| 30 | this.config = { | 43 | this.config = { | ||
| 31 | isDark: prefersDark, | 44 | isDark: prefersDark, | ||
| @@ -79,32 +92,60 @@ class ColorSchemeManager { | |||||
| 79 | } | 92 | } | ||
| 80 | 93 | ||||
| 81 | private injectColorSchemeMeta(): void { | 94 | private injectColorSchemeMeta(): void { | ||
| 82 | - | const meta = document.createElement("meta"); | 95 | + | if (this.metaElement && this.metaElement.isConnected) { |
| 96 | + | this.metaElement.content = this.config.metaContent; | |||
| 97 | + | return; | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | const existing = document.querySelector( | |||
| 101 | + | 'meta[name="color-scheme"]', | |||
| 102 | + | ) as HTMLMetaElement | null; | |||
| 103 | + | const meta = existing ?? document.createElement("meta"); | |||
| 83 | meta.name = "color-scheme"; | 104 | meta.name = "color-scheme"; | ||
| 84 | meta.content = this.config.metaContent; | 105 | meta.content = this.config.metaContent; | ||
| 85 | - | document.documentElement.appendChild(meta); | 106 | + | if (!existing) { |
| 107 | + | document.documentElement.appendChild(meta); | |||
| 108 | + | } | |||
| 109 | + | this.metaElement = meta; | |||
| 86 | } | 110 | } | ||
| 87 | 111 | ||||
| 88 | private applyRootStyles(): void { | 112 | private applyRootStyles(): void { | ||
| 89 | - | const style = document.createElement("style"); | 113 | + | if (this.styleElement && this.styleElement.isConnected) { |
| 114 | + | this.styleElement.textContent = this.config.rootStyle; | |||
| 115 | + | return; | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | const existing = document.querySelector( | |||
| 119 | + | "style[data-dumber-color-scheme]", | |||
| 120 | + | ) as HTMLStyleElement | null; | |||
| 121 | + | const style = existing ?? document.createElement("style"); | |||
| 122 | + | style.setAttribute("data-dumber-color-scheme", ""); | |||
| 90 | style.textContent = this.config.rootStyle; | 123 | style.textContent = this.config.rootStyle; | ||
| 91 | - | document.documentElement.appendChild(style); | 124 | + | if (!existing) { |
| 125 | + | document.documentElement.appendChild(style); | |||
| 126 | + | } | |||
| 127 | + | this.styleElement = style; | |||
| 92 | } | 128 | } | ||
| 93 | 129 | ||||
| 94 | private overrideMatchMedia(): void { | 130 | private overrideMatchMedia(): void { | ||
| 131 | + | if (ColorSchemeManager.matchMediaPatched) { | |||
| 132 | + | return; | |||
| 133 | + | } | |||
| 134 | + | ||||
| 95 | const darkQuery = "(prefers-color-scheme: dark)"; | 135 | const darkQuery = "(prefers-color-scheme: dark)"; | ||
| 96 | const lightQuery = "(prefers-color-scheme: light)"; | 136 | const lightQuery = "(prefers-color-scheme: light)"; | ||
| 97 | - | const originalMatchMedia = window.matchMedia; | 137 | + | ColorSchemeManager.originalMatchMedia = window.matchMedia.bind(window); |
| 98 | 138 | ||||
| 99 | window.matchMedia = (query: string): MediaQueryList => { | 139 | window.matchMedia = (query: string): MediaQueryList => { | ||
| 140 | + | const manager = ColorSchemeManager.instance; | |||
| 100 | if ( | 141 | if ( | ||
| 142 | + | manager && | |||
| 101 | typeof query === "string" && | 143 | typeof query === "string" && | ||
| 102 | (query.includes(darkQuery) || query.includes(lightQuery)) | 144 | (query.includes(darkQuery) || query.includes(lightQuery)) | ||
| 103 | ) { | 145 | ) { | ||
| 104 | - | // Create a mock MediaQueryList that reflects our GTK preference | |||
| 105 | const matches = query.includes("dark") | 146 | const matches = query.includes("dark") | ||
| 106 | - | ? this.config.isDark | 147 | + | ? manager.config.isDark |
| 107 | - | : !this.config.isDark; | 148 | + | : !manager.config.isDark; |
| 108 | 149 | ||||
| 109 | return { | 150 | return { | ||
| 110 | matches, | 151 | matches, | ||
| @@ -118,9 +159,15 @@ class ColorSchemeManager { | |||||
| 118 | } as MediaQueryList; | 159 | } as MediaQueryList; | ||
| 119 | } | 160 | } | ||
| 120 | 161 | ||||
| 121 | - | // For non-color-scheme queries, use the original implementation | 162 | + | const original = ColorSchemeManager.originalMatchMedia; |
| 122 | - | return originalMatchMedia.call(window, query); | 163 | + | if (!original) { |
| 164 | + | throw new Error("Original matchMedia not available"); | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | return original.call(window, query); | |||
| 123 | }; | 168 | }; | ||
| 169 | + | ||||
| 170 | + | ColorSchemeManager.matchMediaPatched = true; | |||
| 124 | } | 171 | } | ||
| 125 | 172 | ||||
| 126 | private setupThemeIntegration(): void { | 173 | private setupThemeIntegration(): void { | ||
| @@ -160,6 +207,42 @@ class ColorSchemeManager { | |||||
| 160 | // Re-apply all configurations | 207 | // Re-apply all configurations | ||
| 161 | this.initialize(); | 208 | this.initialize(); | ||
| 162 | } | 209 | } | ||
| 210 | + | ||||
| 211 | + | private getStoredPreference(): "light" | "dark" | null { | |||
| 212 | + | try { | |||
| 213 | + | const value = localStorage.getItem(ColorSchemeManager.STORAGE_KEY); | |||
| 214 | + | if (value === "light" || value === "dark") { | |||
| 215 | + | return value; | |||
| 216 | + | } | |||
| 217 | + | } catch (error) { | |||
| 218 | + | console.warn("[dumber] Failed to read stored theme preference", error); | |||
| 219 | + | } | |||
| 220 | + | return null; | |||
| 221 | + | } | |||
| 222 | + | ||||
| 223 | + | private storePreference(theme: "light" | "dark"): void { | |||
| 224 | + | try { | |||
| 225 | + | localStorage.setItem(ColorSchemeManager.STORAGE_KEY, theme); | |||
| 226 | + | } catch (error) { | |||
| 227 | + | console.warn("[dumber] Failed to persist theme preference", error); | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | /** | |||
| 232 | + | * Persist and apply an explicit user preference | |||
| 233 | + | */ | |||
| 234 | + | public setUserPreference(theme: "light" | "dark"): void { | |||
| 235 | + | if (this.userPreference === theme && this.config.colorScheme === theme) { | |||
| 236 | + | return; | |||
| 237 | + | } | |||
| 238 | + | this.userPreference = theme; | |||
| 239 | + | this.storePreference(theme); | |||
| 240 | + | this.updateColorScheme(theme === "dark"); | |||
| 241 | + | } | |||
| 242 | + | ||||
| 243 | + | public getCurrentTheme(): "light" | "dark" { | |||
| 244 | + | return this.config.colorScheme; | |||
| 245 | + | } | |||
| 163 | } | 246 | } | ||
| 164 | 247 | ||||
| 165 | // Initialize color scheme management | 248 | // Initialize color scheme management | ||
| @@ -169,3 +252,12 @@ const colorSchemeManager = new ColorSchemeManager(); | |||||
| 169 | (window as any).__dumber_color_scheme_manager = colorSchemeManager; | 252 | (window as any).__dumber_color_scheme_manager = colorSchemeManager; | ||
| 170 | 253 | ||||
| 171 | export default colorSchemeManager; | 254 | export default colorSchemeManager; | ||
| 255 | + | ||||
| 256 | + | declare global { | |||
| 257 | + | interface Window { | |||
| 258 | + | __dumber_color_scheme_manager?: { | |||
| 259 | + | setUserPreference: (theme: "light" | "dark") => void; | |||
| 260 | + | getCurrentTheme: () => "light" | "dark"; | |||
| 261 | + | }; | |||
| 262 | + | } | |||
| 263 | + | } | |||