Preserve hosted website UX inside Archipelago, add isolated Business and owner-authorized native wallet setup, and include deployment handoff. Based on main with native signer import already accepted.
105 lines
5.3 KiB
JavaScript
105 lines
5.3 KiB
JavaScript
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 `<button type="button" class="module-qr" data-qr-trigger="${escapeHtml(id)}" ${free ? 'data-free-qr' : 'data-module-qr'}="${escapeHtml(id)}" data-qr-name="${escapeHtml(name)}" data-qr-url="${escapeHtml(url)}" aria-label="Show ${escapeHtml(name)} QR code" aria-haspopup="dialog" title="Show ${escapeHtml(name)} QR code" ${hidden ? 'hidden' : ''}><img src="${qrImageUrl(url)}" alt="" loading="lazy"><span>QR</span></button>`;
|
||
}
|
||
|
||
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 = `<button type="button" class="module-qr-close" aria-label="Close QR code" autofocus>×</button><p class="eyebrow">Quick access</p><h2 id="module-qr-title"></h2><p id="module-qr-description">Scan to open this merchant’s module on your phone.</p><div class="module-qr-image"><img alt=""><p role="status" aria-live="polite"></p></div><a class="module-qr-url" target="_blank" rel="noopener"></a><div class="module-qr-tools"><button type="button" data-qr-copy>Copy link</button><a data-qr-download>Download QR</a><button type="button" data-qr-print>Print QR</button></div><p class="module-qr-feedback" role="status" aria-live="polite"></p>`;
|
||
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();
|
||
}
|