accounts
Version:
Tempo Accounts SDK
249 lines • 9.48 kB
JavaScript
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