1+ import { hasUserActivation , openAuthPopup } from '../lib/auth-popup.js' ;
2+
13class PuterDialog extends ( globalThis . HTMLElement || Object ) { // It will fall back to only extending Object in environments without a DOM
24 // Similar to `#messageID` in Auth.js. We start at an arbitrary high number to avoid
35 // collisions.
46 static messageID = Math . floor ( Number . MAX_SAFE_INTEGER / 2 ) ;
57
6- /**
7- * Detects if the current page is loaded using the file:// protocol.
8- * @returns {boolean } True if using file:// protocol, false otherwise.
9- */
10- isUsingFileProtocol = ( ) => {
11- return window . location . protocol === 'file:' ;
12- } ;
13-
148 #messageID;
159
16- constructor ( resolve , reject ) {
10+ /**
11+ * @param {Function } resolve - Resolves the implicit-auth promise.
12+ * @param {Function } reject - Rejects the implicit-auth promise.
13+ * @param {Object } [options] - Optional configuration.
14+ * @param {string } [options.popupURL] - When set, the dialog acts as a
15+ * generic popup launcher: it opens this URL (instead of the default
16+ * implicit-auth URL), skips the `puter.token` message handling and
17+ * `puterAuthState` bookkeeping (the caller owns the auth result), and
18+ * reports cancellation through `options.onCancel`.
19+ * @param {Function } [options.onLaunch] - Called with the opened popup
20+ * window (or null if the browser blocked it) right after launch.
21+ * @param {Function } [options.onCancel] - Called when the user dismisses
22+ * the dialog without completing authentication.
23+ */
24+ constructor ( resolve , reject , options = { } ) {
1725 super ( ) ;
1826 this . reject = reject ;
1927 this . resolve = resolve ;
20- this . popupLaunched = false ; // Track if popup was successfully launched
28+ this . options = options ;
2129 this . #messageID = this . constructor . messageID ++ ;
2230
23- /**
24- * Detects if there's a recent user activation that would allow popup opening
25- * @returns {boolean } True if user activation is available, false otherwise.
26- */
27- this . hasUserActivation = ( ) => {
28- // Modern browsers support navigator.userActivation
29- if ( navigator . userActivation ) {
30- return navigator . userActivation . hasBeenActive && navigator . userActivation . isActive ;
31- }
32-
33- // Fallback: try to detect user activation by attempting to open a popup
34- // This is a bit hacky but works as a fallback
35- try {
36- const testPopup = window . open ( '' , '_blank' , 'width=1,height=1,left=-1000,top=-1000' ) ;
37- if ( testPopup ) {
38- testPopup . close ( ) ;
39- return true ;
40- }
41- return false ;
42- } catch ( e ) {
43- return false ;
44- }
45- } ;
46-
47- /**
48- * Launches the authentication popup window
49- * @returns {Window|null } The popup window reference or null if failed
50- */
51- this . launchPopup = ( ) => {
52- try {
53- let w = 600 ;
54- let h = 700 ;
55- let title = 'Puter' ;
56- var left = ( screen . width / 2 ) - ( w / 2 ) ;
57- var top = ( screen . height / 2 ) - ( h / 2 ) ;
58- const popup = window . open (
59- `${ puter . defaultGUIOrigin } /?embedded_in_popup=true&request_auth=true${ window . crossOriginIsolated ? '&cross_origin_isolated=true' : '' } ` ,
60- title ,
61- `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${ w } , height=${ h } , top=${ top } , left=${ left } ` ,
62- ) ;
63- return popup ;
64- } catch ( e ) {
65- console . error ( 'Failed to open popup:' , e ) ;
66- return null ;
67- }
68- } ;
69-
7031 this . attachShadow ( { mode : 'open' } ) ;
7132
7233 let h ;
@@ -496,10 +457,31 @@ class PuterDialog extends (globalThis.HTMLElement || Object) { // It will fall b
496457
497458 }
498459
460+ /**
461+ * Returns the URL to open in the auth popup. In launcher mode this is the
462+ * caller-supplied URL; otherwise it is the default implicit-auth URL.
463+ * @returns {string }
464+ */
465+ #popupURL ( ) {
466+ if ( this . options . popupURL ) {
467+ return this . options . popupURL ;
468+ }
469+ return `${ puter . defaultGUIOrigin } /?embedded_in_popup=true&request_auth=true&msg_id=${ this . #messageID} ${ window . crossOriginIsolated ? '&cross_origin_isolated=true' : '' } ` ;
470+ }
471+
499472 // Optional: Handle dialog cancellation as rejection
500473 cancelListener = ( ) => {
501474 this . close ( ) ;
502475 window . removeEventListener ( 'message' , this . messageListener ) ;
476+
477+ // Launcher mode: the caller owns the auth promise and any state.
478+ if ( this . options . popupURL ) {
479+ if ( typeof this . options . onCancel === 'function' ) {
480+ this . options . onCancel ( ) ;
481+ }
482+ return ;
483+ }
484+
503485 puter . puterAuthState . authGranted = false ;
504486 puter . puterAuthState . isPromptOpen = false ;
505487
@@ -515,40 +497,38 @@ class PuterDialog extends (globalThis.HTMLElement || Object) { // It will fall b
515497 } ;
516498
517499 connectedCallback ( ) {
518- // Add event listener to the button
500+ // Wire the "Continue" button to open the auth popup. Opening here is
501+ // safe from being popup-blocked because it happens inside a click.
519502 this . shadowRoot . querySelector ( '#launch-auth-popup' ) ?. addEventListener ( 'click' , ( ) => {
520- let w = 600 ;
521- let h = 700 ;
522- let title = 'Puter' ;
523- var left = ( screen . width / 2 ) - ( w / 2 ) ;
524- var top = ( screen . height / 2 ) - ( h / 2 ) ;
525- window . open (
526- ` ${ puter . defaultGUIOrigin } /?embedded_in_popup=true&request_auth=true&msg_id= ${ this . #messageID } ${ window . crossOriginIsolated ? '&cross_origin_isolated=true' : '' } ` ,
527- title ,
528- `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width= ${ w } , height= ${ h } , top= ${ top } , left= ${ left } ` ,
529- ) ;
503+ const popup = openAuthPopup ( this . #popupURL ( ) ) ;
504+
505+ // Launcher mode: hand the popup back to the caller and close the
506+ // consent dialog — its only job was to provide the user gesture.
507+ if ( this . options . popupURL ) {
508+ if ( typeof this . options . onLaunch === 'function' ) {
509+ this . options . onLaunch ( popup ) ;
510+ }
511+ this . close ( ) ;
512+ }
530513 } ) ;
531514
532- // Add the event listener to the window object
533- window . addEventListener ( 'message' , this . messageListener ) ;
515+ // The implicit-auth flow listens for the token message from the popup.
516+ // In launcher mode the caller registers its own message handler.
517+ if ( ! this . options . popupURL ) {
518+ window . addEventListener ( 'message' , this . messageListener ) ;
519+ }
534520
535521 // Add event listeners for cancel and close buttons
536522 this . shadowRoot . querySelector ( '#launch-auth-popup-cancel' ) ?. addEventListener ( 'click' , this . cancelListener ) ;
537523 this . shadowRoot . querySelector ( '.close-btn' ) ?. addEventListener ( 'click' , this . cancelListener ) ;
538524 }
539525
540526 open ( ) {
541- if ( this . hasUserActivation ( ) ) {
542- let w = 600 ;
543- let h = 700 ;
544- let title = 'Puter' ;
545- var left = ( screen . width / 2 ) - ( w / 2 ) ;
546- var top = ( screen . height / 2 ) - ( h / 2 ) ;
547- window . open (
548- `${ puter . defaultGUIOrigin } /?embedded_in_popup=true&request_auth=true&msg_id=${ this . #messageID} ${ window . crossOriginIsolated ? '&cross_origin_isolated=true' : '' } ` ,
549- title ,
550- `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${ w } , height=${ h } , top=${ top } , left=${ left } ` ,
551- ) ;
527+ if ( hasUserActivation ( ) ) {
528+ const popup = openAuthPopup ( this . #popupURL( ) ) ;
529+ if ( this . options . popupURL && typeof this . options . onLaunch === 'function' ) {
530+ this . options . onLaunch ( popup ) ;
531+ }
552532 }
553533 else {
554534 this . shadowRoot . querySelector ( 'dialog' ) . showModal ( ) ;
0 commit comments