export function qrImageUrl(text) {
const prefix = location.pathname.startsWith('/business-demo/') ? '/business-demo' : '';
return `${prefix}/api/qr?text=${encodeURIComponent(text)}`;
}
// QR payloads contain only the public destination and merchant context.
export function moduleQrUrl(destination, npub) {
const url = new URL(destination, location.origin);
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Invalid module URL');
const surface = url.searchParams.get('surface');
url.username = '';
url.password = '';
url.search = '';
url.hash = '';
if (surface) url.searchParams.set('surface', surface);
url.searchParams.set('npub', npub);
return url.href;
}
const escapeHtml = value => String(value).replace(/[&<>"']/g, char => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char]));
// Both public links and business modules share the exact same QR control.
// Free-link URLs are already resolved by the API: preserve them verbatim.
export function qrButton(id, name, url, {free = false, hidden = false} = {}) {
return ``;
}
export function mountModuleQrs(root) {
const dialog = document.createElement('dialog');
dialog.className = 'module-qr-dialog';
dialog.setAttribute('aria-labelledby', 'module-qr-title');
dialog.setAttribute('aria-describedby', 'module-qr-description');
dialog.innerHTML = `
Quick access
Scan to open this merchant’s module on your phone.
`;
document.body.append(dialog);
const title = dialog.querySelector('h2');
const img = dialog.querySelector('img');
const status = dialog.querySelector('.module-qr-image [role=status]');
const urlLink = dialog.querySelector('.module-qr-url');
const feedback = dialog.querySelector('.module-qr-feedback');
const download = dialog.querySelector('[data-qr-download]');
const print = dialog.querySelector('[data-qr-print]');
img.onload = () => {
img.hidden = false;
status.textContent = '';
download.hidden = false;
print.disabled = false;
};
img.onerror = () => {
img.hidden = true;
status.textContent = 'QR code could not load. You can still open or copy the link.';
download.hidden = true;
print.disabled = true;
};
root.addEventListener('click', (event) => {
const button = event.target.closest('[data-qr-trigger]');
if (!button || button.hidden) return;
const { qrUrl, qrName, qrTrigger } = button.dataset;
dialog.querySelector('#module-qr-description').textContent = button.hasAttribute('data-free-qr')
? 'Scan to open this merchant’s public page on your phone.'
: 'Scan to open this merchant’s module on your phone.';
title.textContent = qrName;
urlLink.href = qrUrl;
urlLink.textContent = qrUrl;
feedback.textContent = '';
img.hidden = true;
status.textContent = 'Loading QR code…';
print.disabled = true;
download.hidden = true;
img.alt = `QR code for ${qrName}`;
img.src = qrImageUrl(qrUrl);
download.href = img.src;
download.download = `justworks-${qrTrigger}-qr.svg`;
dialog.showModal();
});
dialog.querySelector('.module-qr-close').onclick = () => dialog.close();
dialog.addEventListener('click', (event) => {
if (event.target !== dialog) return;
const bounds = dialog.getBoundingClientRect();
if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) dialog.close();
});
// Native modal dialogs restore focus on close. Do not also restore it from
// the queued `close` event: that can steal focus from the next QR trigger.
dialog.addEventListener('keydown', (event) => {
if (event.key !== 'Tab') return;
const controls = [...dialog.querySelectorAll('button:not(:disabled), a[href]')].filter(node => !node.hidden);
const first = controls[0], last = controls.at(-1);
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
dialog.querySelector('[data-qr-copy]').onclick = async () => {
try {
await navigator.clipboard.writeText(urlLink.href);
feedback.textContent = 'Link copied.';
} catch {
feedback.textContent = 'Could not copy automatically. Select and copy the link above.';
}
};
print.onclick = () => window.print();
}