Files
puter/src/puter-js/src/modules/PuterDialog.js
T
Juan Castro 29c6f78b7c fix: refuse opaque origins cleanly instead of a logged 400
A page the browser gives no origin to — one opened straight from disk, or an
iframe sandboxed without `allow-same-origin` — serialises its origin as
`"null"`. `appUidFromOrigin` logged that at error level on every attempt and
the GUI sent it on three endpoints, so each refusal cost a 400 and an
error-level line while the user got nothing useful.

An opaque origin cannot name an app to mint a token for, and is not a valid
`postMessage` target to deliver one to, so these clients are refused rather
than supported. Make the refusal quiet, early and legible instead:

- AuthService: drop the `console.error` and say what the caller should do.
  The accept/reject set is unchanged; a 400 is already counted per route.
- puter.js: `signIn()` rejects with `unsupported_origin` before opening
  anything, which covers implicit auth too since it routes through `signIn`.
  The load-time warning now covers sandboxed iframes, console-only — a modal
  belongs in nobody else's embed.
- GUI: a popup whose opener has no attested origin says so and closes,
  instead of rendering a blank window and wedging on a failed exchange.

Also fixes two faults this made reachable: `PuterDialog.open()` prefers the
popup branch, which reaches the `puter` global before the constructor that
assigns it has returned (`puter is not defined`, no SDK at all); and
`showModal()` throws where modals are blocked. `openNotice()` does neither.

Ordinary http(s) sign-in is unaffected.
2026-10-01 11:09:04 -04:00

627 lines
26 KiB
JavaScript

import { hasOpaqueOrigin, hasUserActivation, openAuthPopup } from '../lib/auth-popup.js';
import { isFramedDocument } from '../lib/appModeGate.js';
class PuterDialog extends (globalThis.HTMLElement || Object) { // It will fall back to only extending Object in environments without a DOM
// Similar to `#messageID` in Auth.js. We start at an arbitrary high number to avoid
// collisions.
static messageID = Math.floor(Number.MAX_SAFE_INTEGER / 2);
#messageID;
/**
* @param {Function} resolve - Resolves the implicit-auth promise.
* @param {Function} reject - Rejects the implicit-auth promise.
* @param {Object} [options] - Optional configuration.
* @param {string} [options.popupURL] - When set, the dialog acts as a
* generic popup launcher: it opens this URL (instead of the default
* implicit-auth URL), skips the `puter.token` message handling and
* `puterAuthState` bookkeeping (the caller owns the auth result), and
* reports cancellation through `options.onCancel`.
* @param {string} [options.popupName] - Window name for the popup. Give
* each concurrent launcher a distinct name: `window.open()` reuses a
* window that already carries the requested name, so sharing one name
* navigates (and hijacks) a popup another pending flow is waiting on.
* @param {Function} [options.onLaunch] - Called with the opened popup
* window (or null if the browser blocked it) right after launch.
* @param {Function} [options.onCancel] - Called when the user dismisses
* the dialog without completing authentication.
*/
constructor (resolve, reject, options = {}) {
super();
this.reject = reject;
this.resolve = resolve;
this.options = options;
this.#messageID = this.constructor.messageID++;
this.attachShadow({ mode: 'open' });
let h;
// Dialog
h = `
<style>
dialog{
background: transparent;
border: none;
box-shadow: none;
outline: none;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.puter-dialog-content {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 20px;
background: white;
box-shadow: 0 0 9px 1px rgb(0 0 0 / 21%);
padding: 80px 20px;
-webkit-font-smoothing: antialiased;
color: #575762;
position: relative;
background-color: #fff;
}
dialog * {
max-width: 500px;
font-family: "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif;
}
dialog p.about{
text-align: center;
font-size: 17px;
padding: 10px 30px;
font-weight: 400;
-webkit-font-smoothing: antialiased;
color: #1f1f2a;
box-sizing: border-box;
max-width: 400px;
}
dialog .buttons{
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-top: 20px;
text-align: center;
flex-direction: column;
}
.launch-auth-popup-footnote{
font-size: 10px;
color: #666;
margin-top: 10px;
/* footer at the bottom */
position: absolute;
left: 0;
right: 0;
bottom: 20px;
text-align: center;
margin: 0 auto;
max-width: 215px;
}
dialog .close-btn{
position: absolute;
right: 15px;
top: 10px;
font-size: 17px;
color: #8a8a8a8c;
cursor: pointer;
}
dialog .close-btn:hover{
color: #000;
}
/* ------------------------------------
Button
------------------------------------*/
dialog .button {
color: #666666;
background-color: #eeeeee;
border-color: #eeeeee;
font-size: 14px;
text-decoration: none;
text-align: center;
line-height: 40px;
height: 35px;
padding: 0 30px;
margin: 0;
display: inline-block;
appearance: none;
cursor: pointer;
border: none;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-color: #b9b9b9;
border-style: solid;
border-width: 1px;
line-height: 35px;
background: -webkit-gradient(linear, left top, left bottom, from(#f6f6f6), to(#e1e1e1));
background: linear-gradient(#f6f6f6, #e1e1e1);
border-radius: 4px;
outline: none;
-webkit-font-smoothing: antialiased;
}
dialog .button:focus-visible {
border-color: rgb(118 118 118);
}
dialog .button:active, dialog .button.active, dialog .button.is-active, dialog .button.has-open-contextmenu {
text-decoration: none;
background-color: #eeeeee;
border-color: #cfcfcf;
color: #a9a9a9;
-webkit-transition-duration: 0s;
transition-duration: 0s;
-webkit-box-shadow: inset 0 1px 3px rgb(0 0 0 / 20%);
box-shadow: inset 0px 2px 3px rgb(0 0 0 / 36%), 0px 1px 0px white;
}
dialog .button.disabled, dialog .button.is-disabled, dialog .button:disabled {
top: 0 !important;
background: #EEE !important;
border: 1px solid #DDD !important;
text-shadow: 0 1px 1px white !important;
color: #CCC !important;
cursor: default !important;
appearance: none !important;
pointer-events: none;
}
dialog .button-action.disabled, dialog .button-action.is-disabled, dialog .button-action:disabled {
background: #55a975 !important;
border: 1px solid #60ab7d !important;
text-shadow: none !important;
color: #CCC !important;
}
dialog .button-primary.disabled, dialog .button-primary.is-disabled, dialog .button-primary:disabled {
background: #8fc2e7 !important;
border: 1px solid #98adbd !important;
text-shadow: none !important;
color: #f5f5f5 !important;
}
dialog .button-block {
width: 100%;
}
dialog .button-primary {
border-color: #088ef0;
background: -webkit-gradient(linear, left top, left bottom, from(#34a5f8), to(#088ef0));
background: linear-gradient(#34a5f8, #088ef0);
color: white;
}
dialog .button-danger {
border-color: #f00808;
background: -webkit-gradient(linear, left top, left bottom, from(#f83434), to(#f00808));
background: linear-gradient(#f83434, #f00808);
color: white;
}
dialog .button-primary:active, dialog .button-primary.active, dialog .button-primary.is-active, dialog .button-primary-flat:active, dialog .button-primary-flat.active, dialog .button-primary-flat.is-active {
background-color: #2798eb;
border-color: #2798eb;
color: #bedef5;
}
dialog .button-action {
border-color: #08bf4e;
background: -webkit-gradient(linear, left top, left bottom, from(#29d55d), to(#1ccd60));
background: linear-gradient(#29d55d, #1ccd60);
color: white;
}
dialog .button-action:active, dialog .button-action.active, dialog .button-action.is-active, dialog .button-action-flat:active, dialog .button-action-flat.active, dialog .button-action-flat.is-active {
background-color: #27eb41;
border-color: #27eb41;
color: #bef5ca;
}
dialog .button-giant {
font-size: 28px;
height: 70px;
line-height: 70px;
padding: 0 70px;
}
dialog .button-jumbo {
font-size: 24px;
height: 60px;
line-height: 60px;
padding: 0 60px;
}
dialog .button-large {
font-size: 20px;
height: 50px;
line-height: 50px;
padding: 0 50px;
}
dialog .button-normal {
font-size: 16px;
height: 40px;
line-height: 38px;
padding: 0 40px;
}
dialog .button-small {
height: 30px;
line-height: 29px;
padding: 0 30px;
}
dialog .button-tiny {
font-size: 9.6px;
height: 24px;
line-height: 24px;
padding: 0 24px;
}
#launch-auth-popup{
width: 220px;
font-weight: 500;
font-size: 15px;
max-width: 250px;
}
dialog .button-auth{
margin-bottom: 10px;
}
dialog .button-auth-cancel{
background: none !important;
width: 220px;
max-width: 250px;
}
dialog a, dialog a:visited{
color: rgb(0 0 0);
text-decoration: none;
}
dialog a:hover{
text-decoration: underline;
}
@media (max-width:480px) {
.puter-dialog-content{
padding: 50px 20px;
}
dialog p.about{
padding: 10px 0;
}
dialog .button-auth{
width: 100% !important;
margin:0 !important;
margin-bottom: 10px !important;
}
dialog .buttons{
margin-bottom: 20px;
}
}
.error-container h1 {
color: #e74c3c;
font-size: 20px;
text-align: center;
}
.puter-dialog-content a:focus{
outline: none;
}
@media (prefers-color-scheme: dark) {
.puter-dialog-content {
border: 1px solid #2a2a2e;
background: #1e1e22;
background-color: #1e1e22;
color: #d6d6dc;
box-shadow: 0 0 9px 1px rgb(0 0 0 / 60%);
}
dialog p.about {
color: #e4e4ea;
}
dialog .close-btn {
color: #8a8a90;
}
dialog .close-btn:hover {
color: #fff;
}
.launch-auth-popup-footnote {
color: #9a9aa0;
}
dialog .button {
color: #d6d6dc;
background-color: #3a3a40;
border-color: #4a4a50;
background: linear-gradient(#3f3f45, #2e2e34);
-webkit-box-shadow: inset 0px 1px 0px rgb(255 255 255 / 6%), 0 1px 2px rgb(0 0 0 / 40%);
box-shadow: inset 0px 1px 0px rgb(255 255 255 / 6%), 0 1px 2px rgb(0 0 0 / 40%);
}
dialog .button:focus-visible {
border-color: #8a8a90;
}
dialog .button:active, dialog .button.active, dialog .button.is-active, dialog .button.has-open-contextmenu {
background-color: #2a2a30;
border-color: #1f1f24;
color: #8a8a90;
-webkit-box-shadow: inset 0 1px 3px rgb(0 0 0 / 60%);
box-shadow: inset 0px 2px 3px rgb(0 0 0 / 60%), 0px 1px 0px rgb(255 255 255 / 4%);
}
/* The dark-mode 'dialog .button' rule above has the same specificity
as the light-mode 'dialog .button-primary' rule, so without this
override source order wins and the primary button turns gray. */
dialog .button-primary {
border-color: #088ef0;
background: linear-gradient(#34a5f8, #088ef0);
color: white;
}
dialog .button-primary:active, dialog .button-primary.active, dialog .button-primary.is-active {
background-color: #2798eb;
border-color: #2798eb;
color: #bedef5;
}
dialog .button.disabled, dialog .button.is-disabled, dialog .button:disabled {
background: #2a2a30 !important;
border: 1px solid #34343a !important;
text-shadow: none !important;
color: #5a5a60 !important;
}
dialog .button-primary.disabled, dialog .button-primary.is-disabled, dialog .button-primary:disabled {
background: #1f4e74 !important;
border: 1px solid #2a5a82 !important;
color: #8aa4bd !important;
}
dialog .button-action.disabled, dialog .button-action.is-disabled, dialog .button-action:disabled {
background: #1f5a3a !important;
border: 1px solid #2a6a45 !important;
color: #8abda0 !important;
}
dialog a, dialog a:visited {
color: #6ea8ff;
}
.error-container h1 {
color: #ff7466;
}
}
</style>`;
// Error message for a page the browser gives no origin to
if ( hasOpaqueOrigin() ) {
// Framing, not the scheme: a missing `location` is opaque too.
const isFileProtocol = ! isFramedDocument();
h += `<dialog>
<div class="puter-dialog-content" style="padding: 20px 40px; font-size: 15px;">
<span class="close-btn">&#x2715</span>
<div class="error-container">
<h1>Puter.js Error: Unsupported Origin</h1>
<p>${isFileProtocol
? 'It looks like you\'ve opened this file directly in your browser (using the <code style="font-family: monospace;">file:///</code> protocol), which gives the page no origin.'
: 'This page is in an iframe sandboxed without <code style="font-family: monospace;">allow-same-origin</code>, which gives it no origin.'
} Puter.js signs a page in by its origin, so it cannot sign this one in.</p>
<p>To use Puter.js here:</p>
<ul>
${isFileProtocol
? '<li>Serve the page from a local development server (e.g., Python\'s built-in server or Node.js http-server)</li><li>Or upload the files to a web hosting service</li>'
: '<li>Add <code style="font-family: monospace;">allow-same-origin</code> to the iframe\'s <code style="font-family: monospace;">sandbox</code> attribute</li><li>Or load Puter.js from the parent page instead</li>'
}
</ul>
<p class="help-text">If you're not familiar with these options, consider reaching out to your development team or IT support for assistance.</p>
</div>
<p style="margin-top: 30px; border-top: 1px solid #eee; padding-top: 10px; text-align: center; font-size:13px;">
<a href="https://docs.puter.com" target="_blank">Docs</a><span style="margin:10px; color: #CCC;">|</span>
<a href="https://github.com/heyPuter/puter/" target="_blank">Github</a><span style="margin:10px; color: #CCC;">|</span>
<a href="https://discord.com/invite/PQcx7Teh8u" target="_blank">Discord</a>
</p>
</div>
</dialog>`;
} else {
h += `<dialog>
<div class="puter-dialog-content">
<span class="close-btn">&#x2715</span>
<a href="https://puter.com?utm_source=sdk-splash" target="_blank" style="border:none; outline:none; display: block; width: 70px; height: 70px; margin: 0 auto; border-radius: 4px;"><img style="display: block; width: 40px; height: 40px; margin: 0 auto; border-radius: 8px; background-color: #2210d7; padding: 15px;" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogPGcgY2xhc3M9ImxheWVyIj4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPGcgaWQ9InN2Z18xIiBzdHJva2Utd2lkdGg9IjMiIHRyYW5zZm9ybT0icm90YXRlKDkwIDI0IDIzLjk5OTcpIj4KICAgPHBvbHlsaW5lIGZpbGw9Im5vbmUiIGlkPSJzdmdfMiIgcG9pbnRzPSIzOSAyNCAyNSAyNCAyNSAyOCIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS13aWR0aD0iMyIvPgogICA8cG9seWxpbmUgZmlsbD0ibm9uZSIgaWQ9InN2Z18zIiBwb2ludHM9IjM1Ljg3OSAxMC4xMjEgMzIgMTQgMjUgMTQgMjUgMTgiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTEzLDI2YTEwLjI5LDEwLjI5IDAgMCAxIC03LjIsLTMiIGZpbGw9Im5vbmUiIGlkPSJzdmdfNCIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS13aWR0aD0iMyIvPgogICA8cGF0aCBkPSJtMTcsMzEuNmE1LjgzLDUuODMgMCAwIDEgLTQsLTUuNmE1LjczLDUuNzMgMCAwIDEgMiwtNC40IiBmaWxsPSJub25lIiBpZD0ic3ZnXzUiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2Utd2lkdGg9IjMiLz4KICAgPHBhdGggZD0ibTM1Ljg4LDM3Ljg4bC0zLjg4LC0zLjg4bC03LDBsMCwyYTkuOSw5LjkgMCAwIDEgLTEwLDEwYTkuOSw5LjkgMCAwIDEgLTEwLC0xMGE5LjA2LDkuMDYgMCAwIDEgMC42LC0zLjJhNS42Myw1LjYzIDAgMCAxIC0yLjYsLTQuOGE1Ljg5LDUuODkgMCAwIDEgMi44LC01YTkuOTksOS45OSAwIDAgMSAtMi44LC03YTkuOSw5LjkgMCAwIDEgMTAsLTEwbDAuNCwwYTUuODMsNS44MyAwIDAgMSA1LjYsLTRhNS44OSw1Ljg5IDAgMCAxIDYsNiIgZmlsbD0ibm9uZSIgaWQ9InN2Z182IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIzIi8+CiAgIDxjaXJjbGUgY3g9IjM4IiBjeT0iOCIgZGF0YS1jb2xvcj0iY29sb3ItMiIgZmlsbD0ibm9uZSIgaWQ9InN2Z183IiByPSIzIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIzIi8+CiAgIDxjaXJjbGUgY3g9IjQyIiBjeT0iMjQiIGRhdGEtY29sb3I9ImNvbG9yLTIiIGZpbGw9Im5vbmUiIGlkPSJzdmdfOCIgcj0iMyIgc3Ryb2tlPSIjZmZmZmZmIiBzdHJva2UtbGluZWNhcD0ic3F1YXJlIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS13aWR0aD0iMyIvPgogICA8Y2lyY2xlIGN4PSIzOCIgY3k9IjQwIiBkYXRhLWNvbG9yPSJjb2xvci0yIiBmaWxsPSJub25lIiBpZD0ic3ZnXzkiIHI9IjMiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2Utd2lkdGg9IjMiLz4KICA8L2c+CiA8L2c+Cjwvc3ZnPg=="/></a>
<p class="about">This website uses Puter to bring you safe, secure, and private AI and Cloud features.</p>
<div class="buttons">
<button class="button button-primary button-auth" id="launch-auth-popup">Continue</button>
<button class="button button-auth button-auth-cancel" id="launch-auth-popup-cancel">Cancel</button>
</div>
<p class="launch-auth-popup-footnote">By clicking 'Continue' you agree to Puter's <a href="https://puter.com/terms" target="_blank">Terms of Service</a> and <a href="https://puter.com/privacy" target="_blank">Privacy Policy</a></p>
</div>
</dialog>`;
}
this.shadowRoot.innerHTML = h;
// Event listener for the 'message' event
this.messageListener = async (event) => {
// Only accept the token from the Puter GUI origin AND from the
// popup we opened. Origin alone is insufficient (any frame on the
// GUI domain could post), so also pin event.source. Mirrors the
// validated handler in index.js.
if ( event.origin !== puter.defaultGUIOrigin ) {
return;
}
if ( this.authPopup && event.source !== this.authPopup ) {
return;
}
if ( event.data.msg === 'puter.token' ) {
this.close();
// Set the authToken property
puter.setAuthToken(event.data.token);
// update appID
puter.setAppID(event.data.app_uid);
// Remove the event listener to avoid memory leaks
window.removeEventListener('message', this.messageListener);
puter.puterAuthState.authGranted = true;
// Resolve the promise
this.resolve();
// Call onAuth callback
if ( puter.onAuth && typeof puter.onAuth === 'function' ) {
puter.getUser().then((user) => {
puter.onAuth(user);
});
}
puter.puterAuthState.isPromptOpen = false;
// Resolve or reject every waiting promise, not just one.
const resolvers = puter.puterAuthState.resolvers;
puter.puterAuthState.resolvers = [];
for ( const resolver of resolvers ) {
if ( puter.puterAuthState.authGranted ) {
resolver.resolve();
} else {
resolver.reject();
}
}
}
};
}
/**
* Returns the URL to open in the auth popup. In launcher mode this is the
* caller-supplied URL; otherwise it is the default implicit-auth URL.
* @returns {string}
*/
#popupURL () {
if ( this.options.popupURL ) {
return this.options.popupURL;
}
return `${puter.defaultGUIOrigin}/?embedded_in_popup=true&request_auth=true&msg_id=${this.#messageID}${window.crossOriginIsolated ? '&cross_origin_isolated=true' : ''}`;
}
// Optional: Handle dialog cancellation as rejection
cancelListener = () => {
this.close();
window.removeEventListener('message', this.messageListener);
// Launcher mode: the caller owns the auth promise and any state.
if ( this.options.popupURL ) {
if ( typeof this.options.onCancel === 'function' ) {
this.options.onCancel();
}
return;
}
puter.puterAuthState.authGranted = false;
puter.puterAuthState.isPromptOpen = false;
// Reject the promise with an error message indicating user cancellation.
// This ensures that the calling code's catch block will be triggered.
this.reject(new Error('User cancelled the authentication'));
// Reject every waiting promise the same way, not just one.
const resolvers = puter.puterAuthState.resolvers;
puter.puterAuthState.resolvers = [];
for ( const resolver of resolvers ) {
resolver.reject(new Error('User cancelled the authentication'));
}
};
connectedCallback () {
// Wire the "Continue" button to open the auth popup. Opening here is
// safe from being popup-blocked because it happens inside a click.
this.shadowRoot.querySelector('#launch-auth-popup')?.addEventListener('click', () => {
const popup = this.#openPopup();
// Pinned as the expected event.source in messageListener.
this.authPopup = popup;
// Launcher mode: hand the popup back to the caller and close the
// consent dialog — its only job was to provide the user gesture.
if ( this.options.popupURL ) {
if ( typeof this.options.onLaunch === 'function' ) {
this.options.onLaunch(popup);
}
this.close();
}
});
// The implicit-auth flow listens for the token message from the popup.
// In launcher mode the caller registers its own message handler.
if ( ! this.options.popupURL ) {
window.addEventListener('message', this.messageListener);
}
// Add event listeners for cancel and close buttons
this.shadowRoot.querySelector('#launch-auth-popup-cancel')?.addEventListener('click', this.cancelListener);
this.shadowRoot.querySelector('.close-btn')?.addEventListener('click', this.cancelListener);
// Escape dismisses a modal <dialog> natively, which would otherwise
// remove the dialog from view without telling the caller — leaving the
// promise behind it pending forever. `cancel` fires only for that
// user-initiated dismissal; a programmatic `close()` (how launching the
// popup dismisses this dialog) fires `close` alone, so the launch path
// is unaffected.
this.shadowRoot.querySelector('dialog')?.addEventListener('cancel', (e) => {
e.preventDefault();
this.cancelListener();
});
}
/**
* Opens the popup under the caller's window name when one was given, so
* concurrent launchers don't reuse (and steal) each other's window.
* @returns {Window|null}
*/
#openPopup () {
return this.options.popupName
? openAuthPopup(this.#popupURL(), this.options.popupName)
: openAuthPopup(this.#popupURL());
}
/**
* Shows the dialog as a notice: no popup to offer, and no throw where
* modals are blocked (`<iframe sandbox>` without `allow-modals`).
*/
openNotice () {
try {
this.shadowRoot.querySelector('dialog').showModal();
} catch (e) {
console.error('Puter.js: could not show the notice dialog', e);
}
}
open () {
if ( hasUserActivation() ) {
const popup = this.#openPopup();
// Pinned as the expected event.source in messageListener.
this.authPopup = popup;
if ( this.options.popupURL && typeof this.options.onLaunch === 'function' ) {
this.options.onLaunch(popup);
}
}
else {
this.shadowRoot.querySelector('dialog').showModal();
}
}
close () {
this.shadowRoot.querySelector('dialog').close();
}
}
if ( PuterDialog.__proto__ === globalThis.HTMLElement )
{
customElements.define('puter-dialog', PuterDialog);
}
export default PuterDialog;