mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-10-25 13:59:36 +00:00
08e6801fc4
Widget: - home background stays pinned while the content scrolls, fade sits on it - one radius and shadow scale across cards, buttons and inputs - live search in the help tab with a "no results" state - attachment-only messages show "Image" in the conversation preview - launcher and panel use the same shadows as the header Admin: - preview column is a fixed width so zoom does not resize it, hidden on small screens - preview matches the real widget again - search spinner replaces the magnifier instead of covering the page Fixes: - deleting a proactive message did nothing, the dialog cleared the pending id before the handler read it - an inbox created through the API could not be saved from the form, it jumped to the pre-chat tab with no error Adds livechatWidgetTabs.cy.js covering the proactive message and help center tabs.
192 lines
7.3 KiB
HTML
192 lines
7.3 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Libredesk widget - JWT test</title>
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 720px; margin: 40px auto; padding: 0 16px; color: #222; }
|
|
@media (max-width: 640px) {
|
|
body { margin: 16px auto; }
|
|
.row { flex-direction: column; gap: 0; }
|
|
}
|
|
h1 { font-size: 18px; }
|
|
label { display: block; margin-top: 14px; font-size: 13px; font-weight: 600; color: #444; }
|
|
input, textarea { width: 100%; box-sizing: border-box; font-family: ui-monospace, monospace; font-size: 13px; padding: 8px; margin-top: 4px; border: 1px solid #ccc; border-radius: 6px; }
|
|
textarea { min-height: 220px; resize: vertical; }
|
|
button { margin-top: 14px; padding: 8px 16px; font-size: 14px; border-radius: 6px; border: 1px solid #333; background: #222; color: #fff; cursor: pointer; }
|
|
button:hover { background: #000; }
|
|
#status { margin-top: 10px; font-size: 13px; white-space: pre-wrap; }
|
|
#jwtOut { min-height: 70px; font-size: 11px; }
|
|
.row { display: flex; gap: 12px; }
|
|
.row > div { flex: 1; }
|
|
.hint { font-size: 12px; color: #777; font-weight: 400; }
|
|
.field-hint { display: block; margin-top: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>Libredesk widget - JWT auth test</h1>
|
|
<p class="hint">Edit the payload below, then load the widget. The signing secret must match the "Secret Key" configured in the inbox form.</p>
|
|
|
|
<div class="row">
|
|
<div>
|
|
<label>baseURL</label>
|
|
<input id="baseURL" value="http://localhost:8001">
|
|
</div>
|
|
<div>
|
|
<label for="inboxID">Inbox UUID</label>
|
|
<input id="inboxID" aria-describedby="inboxIDHint" value="">
|
|
<span id="inboxIDHint" class="hint field-hint">Copy the UUID from your inbox form.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<label style="display: flex; align-items: center; gap: 8px; margin-top: 18px;">
|
|
<input type="checkbox" id="useJwt" style="width: auto; margin: 0;" checked>
|
|
Authenticate with JWT <span class="hint">(uncheck to start chat as an anonymous visitor, no JWT)</span>
|
|
</label>
|
|
|
|
<div id="jwtFields">
|
|
<label>Signing secret <span class="hint">(inbox's Secret Key, HS256)</span></label>
|
|
<input id="secret" placeholder="inbox Secret Key" value="">
|
|
|
|
<label>JWT payload <span class="hint">(edit freely - exp is overwritten automatically)</span></label>
|
|
<textarea id="payload">{
|
|
"external_user_id": "your_app_user_123",
|
|
"email": "user@example.com",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"phone_number": "9876543210",
|
|
"phone_number_country_code": "IN",
|
|
"contact_custom_attributes": {
|
|
"plan": "premium",
|
|
"company": "Acme Inc"
|
|
}
|
|
}</textarea>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<button id="genBtn">Load widget</button>
|
|
<button id="clearBtn" style="background: #fff; color: #333;">Clear session</button>
|
|
</div>
|
|
<p class="hint">The widget stores its session/visitor tokens as cookies on this page (plus in-memory state inside the iframe) - reloading the widget alone does not drop them. Use "Clear session" first to actually start over as a brand-new visitor.</p>
|
|
|
|
<div id="status"></div>
|
|
|
|
<label>Generated JWT</label>
|
|
<textarea id="jwtOut" readonly></textarea>
|
|
|
|
<script>
|
|
function base64url(bytes) {
|
|
let binary = '';
|
|
bytes.forEach(b => binary += String.fromCharCode(b));
|
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
async function signJWT(payload, secret) {
|
|
const enc = new TextEncoder();
|
|
const header = { alg: 'HS256', typ: 'JWT' };
|
|
const headerB64 = base64url(enc.encode(JSON.stringify(header)));
|
|
const payloadB64 = base64url(enc.encode(JSON.stringify(payload)));
|
|
const signingInput = headerB64 + '.' + payloadB64;
|
|
const key = await crypto.subtle.importKey(
|
|
'raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
|
|
);
|
|
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(signingInput));
|
|
return signingInput + '.' + base64url(new Uint8Array(sig));
|
|
}
|
|
|
|
function loadWidgetScript(baseURL) {
|
|
return new Promise((resolve, reject) => {
|
|
if (window.__widgetScriptLoaded) return resolve();
|
|
const s = document.createElement('script');
|
|
s.src = baseURL + '/widget.js';
|
|
s.onload = () => { window.__widgetScriptLoaded = true; resolve(); };
|
|
s.onerror = reject;
|
|
document.body.appendChild(s);
|
|
});
|
|
}
|
|
|
|
function deleteWidgetCookies(inboxID) {
|
|
['session', 'visitor'].forEach(type => {
|
|
document.cookie = `libredesk-${type}-${inboxID}=;path=/;max-age=0;SameSite=Lax`;
|
|
});
|
|
}
|
|
|
|
document.getElementById('clearBtn').addEventListener('click', () => {
|
|
const inboxID = document.getElementById('inboxID').value.trim();
|
|
if (window.Libredesk && typeof window.Libredesk.logout === 'function') {
|
|
window.Libredesk.logout();
|
|
}
|
|
deleteWidgetCookies(inboxID);
|
|
if (window.Libredesk && typeof window.Libredesk.destroy === 'function') {
|
|
window.Libredesk.destroy();
|
|
window.Libredesk = undefined;
|
|
}
|
|
document.getElementById('status').textContent = 'Session cleared. Click "Load widget" to start fresh.';
|
|
});
|
|
|
|
const useJwtEl = document.getElementById('useJwt');
|
|
const jwtFieldsEl = document.getElementById('jwtFields');
|
|
|
|
function syncJwtFieldsVisibility () {
|
|
jwtFieldsEl.style.display = useJwtEl.checked ? '' : 'none';
|
|
}
|
|
useJwtEl.checked = localStorage.getItem('libredesk-jwt-test-use-jwt') !== 'false';
|
|
useJwtEl.addEventListener('change', () => {
|
|
localStorage.setItem('libredesk-jwt-test-use-jwt', useJwtEl.checked);
|
|
syncJwtFieldsVisibility();
|
|
});
|
|
syncJwtFieldsVisibility();
|
|
|
|
// Remember every field's last value across reloads so a refresh doesn't wipe the form.
|
|
['baseURL', 'inboxID', 'secret', 'payload'].forEach(id => {
|
|
const el = document.getElementById(id);
|
|
const saved = localStorage.getItem('libredesk-jwt-test-' + id);
|
|
if (saved !== null) el.value = saved;
|
|
el.addEventListener('input', () => {
|
|
localStorage.setItem('libredesk-jwt-test-' + id, el.value);
|
|
});
|
|
});
|
|
|
|
document.getElementById('genBtn').addEventListener('click', async () => {
|
|
const statusEl = document.getElementById('status');
|
|
const jwtOut = document.getElementById('jwtOut');
|
|
statusEl.textContent = '';
|
|
try {
|
|
const baseURL = document.getElementById('baseURL').value.trim();
|
|
const inboxID = document.getElementById('inboxID').value.trim();
|
|
const config = { baseURL, inboxID };
|
|
let statusMsg = 'Widget loaded as anonymous visitor (no JWT).';
|
|
|
|
if (useJwtEl.checked) {
|
|
const secret = document.getElementById('secret').value;
|
|
const payload = JSON.parse(document.getElementById('payload').value);
|
|
payload.exp = Math.floor(Date.now() / 1000) + 3600;
|
|
|
|
const jwt = await signJWT(payload, secret);
|
|
jwtOut.value = jwt;
|
|
config.userJWT = jwt;
|
|
statusMsg = 'Widget loaded with new JWT. exp=' + new Date(payload.exp * 1000).toLocaleTimeString();
|
|
} else {
|
|
jwtOut.value = '';
|
|
}
|
|
|
|
await loadWidgetScript(baseURL);
|
|
|
|
if (window.Libredesk && typeof window.Libredesk.destroy === 'function') {
|
|
window.Libredesk.destroy();
|
|
window.Libredesk = undefined;
|
|
}
|
|
window.initLibredesk(config);
|
|
|
|
statusEl.textContent = statusMsg;
|
|
} catch (err) {
|
|
statusEl.textContent = 'Error: ' + err.message;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|