UNPKG

accounts

Version:

Tempo Accounts SDK

249 lines 9.48 kB
import { defaultSize, isInsecureContext } from '../../Dialog.js'; import * as IO from '../../IntersectionObserver.js'; import * as TrustedHosts from '../../TrustedHosts.js'; /** * Picks the default mount: an overlay iframe unless the context can't * support one — insecure contexts lack WebAuthn in iframes, and without * IntersectionObserver v2 an untrusted host can't run its occlusion * defense. */ export function auto(options = {}) { if (typeof window === 'undefined') return popup(); if (isInsecureContext()) return popup(); const trusted = (() => { const trustedHosts = options.trustedHosts ?? trustedHostsFor(options.source); if (!trustedHosts) return false; const hostname = window.location.hostname.replace(/^www\./, ''); return TrustedHosts.match(trustedHosts, hostname, options.source); })(); if (!IO.supported() && !trusted) return popup(); return iframe(); } function trustedHostsFor(source) { if (!source) return undefined; const hostname = source.replace(/^www\./, ''); for (const [source_, trustedHosts] of Object.entries(TrustedHosts.hosts)) if (TrustedHosts.sameRegistrableDomain(hostname, source_)) return trustedHosts; return undefined; } /** Mounts the wallet page in a hidden full-viewport overlay iframe. */ export function iframe() { return Object.assign((parameters) => { const { host, onDismiss, onInvalidate } = parameters; const url = new URL(host); const origin = url.origin; const scheme = url.searchParams.get('scheme'); const root = document.createElement('dialog'); // Distinct marker from the bespoke `Dialog.iframe` (`data-tempo-wallet`) // so the two mount systems never collide in the DOM or in cleanup. root.dataset.tempoWalletPostmessage = ''; root.setAttribute('role', 'dialog'); root.setAttribute('aria-closed', 'true'); root.setAttribute('aria-label', 'Tempo Wallet'); root.setAttribute('hidden', 'until-found'); Object.assign(root.style, { background: 'transparent', border: '0', outline: '0', padding: '0', position: 'fixed', }); const frame = document.createElement('iframe'); frame.dataset.testid = 'tempo-wallet-postmessage'; frame.setAttribute('allow', [ `publickey-credentials-get ${origin}`, `publickey-credentials-create ${origin}`, 'clipboard-write', 'payment', ].join('; ')); frame.setAttribute('allowtransparency', 'true'); frame.setAttribute('tabindex', '0'); frame.setAttribute('title', 'Tempo Wallet'); frame.src = host; Object.assign(frame.style, { backgroundColor: 'transparent', border: '0', colorScheme: scheme === 'light' || scheme === 'dark' ? scheme : 'light dark', height: '100%', left: '0', position: 'fixed', top: '0', width: '100%', }); const style = document.createElement('style'); style.innerHTML = ` dialog[data-tempo-wallet-postmessage]::backdrop { background: transparent!important; } `; root.appendChild(style); root.appendChild(frame); document.body.appendChild(root); // Re-mount if removed (e.g. React hydration clears non-server-rendered // elements). Re-appending reloads the wallet page, so the session must // re-handshake through a fresh handle. const remountObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.removedNodes) { if (node !== root) continue; document.body.appendChild(root); onInvalidate(); return; } } }); remountObserver.observe(document.body, { childList: true }); // 1Password extension adds `inert` to `dialog`, rendering it unusable. const inertObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type !== 'attributes') continue; if (mutation.attributeName !== 'inert') continue; root.removeAttribute('inert'); } }); inertObserver.observe(root, { attributeOldValue: true, attributes: true }); const onCancel = (event) => { event.preventDefault(); onDismiss(); }; const onClick = () => onDismiss(); let opener = null; let savedOverflow = ''; let visible = false; function show() { if (visible) return frame.focus(); visible = true; if (document.activeElement instanceof HTMLElement) opener = document.activeElement; root.removeAttribute('hidden'); root.removeAttribute('aria-closed'); root.showModal(); root.addEventListener('cancel', onCancel); root.addEventListener('click', onClick); root.style.pointerEvents = 'auto'; frame.focus(); savedOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; } function hide() { if (!visible) return; visible = false; root.removeEventListener('cancel', onCancel); root.removeEventListener('click', onClick); root.style.pointerEvents = 'none'; root.setAttribute('hidden', 'true'); root.setAttribute('aria-closed', 'true'); root.close(); opener?.focus(); opener = null; document.body.style.overflow = savedOverflow; // 1Password sometimes adds `inert` to dialog siblings and does not // clean up when the dialog closes. for (const sibling of root.parentNode ? Array.from(root.parentNode.children) : []) { if (sibling === root) continue; if (sibling.hasAttribute('inert')) sibling.removeAttribute('inert'); } } return { close() { hide(); }, destroy() { hide(); remountObserver.disconnect(); inertObserver.disconnect(); root.remove(); }, hide, mode: 'iframe', show, target: () => frame.contentWindow, }; }, { mode: 'iframe' }); } /** Opens the wallet page in a centered popup window per request burst. */ export function popup(options = {}) { const { size = defaultSize } = options; return Object.assign((parameters) => { const { host, onDismiss } = parameters; const overlay = document.createElement('div'); Object.assign(overlay.style, { alignItems: 'center', background: 'rgba(0, 0, 0, 0.5)', color: 'white', display: 'none', flexDirection: 'column', fontFamily: 'system-ui, sans-serif', fontSize: '16px', gap: '12px', inset: '0', justifyContent: 'center', position: 'fixed', zIndex: '2147483647', }); const message = document.createElement('p'); Object.assign(message.style, { margin: '0' }); message.textContent = 'Continue in the popup window'; const close = document.createElement('button'); Object.assign(close.style, { background: 'none', border: 'none', color: 'white', cursor: 'pointer', font: 'inherit', padding: '0', textDecoration: 'underline', }); close.textContent = 'Close'; close.addEventListener('click', () => onDismiss()); overlay.appendChild(message); overlay.appendChild(close); document.body.appendChild(overlay); let win = null; function hide() { overlay.style.display = 'none'; win?.close(); win = null; } return { close() { hide(); }, destroy() { hide(); overlay.remove(); }, hide, mode: 'popup', show() { if (win && !win.closed) win.focus(); }, target: () => { if (win && !win.closed) return win; const left = (window.innerWidth - size.width) / 2 + window.screenX; const top = (window.innerHeight - size.height) / 2 + window.screenY; const features = `width=${size.width},height=${size.height},left=${left},top=${top}`; win = window.open(host, '_blank', features); if (win) overlay.style.display = 'flex'; return win; }, }; }, { mode: 'popup' }); } //# sourceMappingURL=mount.js.map