Add onboarding weather and template gradients
This commit is contained in:
+186
-13
@@ -1,20 +1,44 @@
|
||||
// Player onboarding routes and signup flow helpers.
|
||||
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const { isClientNameAvailable, withClientNameReservation } = require('#src/data/client-name-check');
|
||||
const { createStyledQrCodeSvg } = require('#src/data/qr-code');
|
||||
const { getSharedSecret, verifyPageAuthToken, createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { getSharedSecret, verifyPageAuthToken, verifyRequestAuth, createRequestAuthHeaders } = require('#src/request-auth');
|
||||
const { resolvePlayerRegistration, upsertPlayerRegistration: upsertPlayerRegistrationRecord } = require('#src/data/player-registry');
|
||||
const { isTransientDbError } = require('./store');
|
||||
|
||||
const ONBOARDING_SIGNUP_LIMIT_WINDOW_MS = 5 * 60 * 1000;
|
||||
const ONBOARDING_SIGNUP_LIMIT_MAX_ATTEMPTS = 8;
|
||||
const PAIRING_CODE_LENGTH = 6;
|
||||
const PAIRING_CODE_TTL_MS = 15 * 60 * 1000;
|
||||
const PAIRING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
const onboardingSignupAttempts = new Map();
|
||||
|
||||
function normalizeDeviceId(value) {
|
||||
return String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128);
|
||||
}
|
||||
|
||||
function createPairingCode() {
|
||||
const bytes = crypto.randomBytes(PAIRING_CODE_LENGTH);
|
||||
let code = '';
|
||||
for (let index = 0; index < PAIRING_CODE_LENGTH; index += 1) {
|
||||
code += PAIRING_CODE_ALPHABET[bytes[index] % PAIRING_CODE_ALPHABET.length];
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function createPairingSession(deviceId, clientId) {
|
||||
return { deviceId: normalizeDeviceId(deviceId), clientId: normalizeDeviceId(clientId), code: createPairingCode(), expiresAt: Date.now() + PAIRING_CODE_TTL_MS };
|
||||
}
|
||||
|
||||
function isValidOnboardingPairingCode(pairingSession, pairingCode, now) {
|
||||
const suppliedCode = Buffer.from(String(pairingCode || '').trim().toUpperCase());
|
||||
const expectedCode = Buffer.from(String(pairingSession && pairingSession.code || '').trim());
|
||||
const currentTime = Number(now || Date.now());
|
||||
return Boolean(pairingSession && pairingSession.deviceId && pairingSession.expiresAt > currentTime && suppliedCode.length === expectedCode.length && suppliedCode.length > 0 && crypto.timingSafeEqual(suppliedCode, expectedCode));
|
||||
}
|
||||
|
||||
function getPublicBaseUrl(req, configuredUrl) {
|
||||
const forwardedProto = String(req.headers['x-forwarded-proto'] || '').trim().split(',')[0];
|
||||
const protocol = forwardedProto || (req.socket && req.socket.encrypted ? 'https' : 'http');
|
||||
@@ -120,7 +144,7 @@ async function commitDeviceBinding(pool, deviceId, clientName, screenSlug, isNam
|
||||
}
|
||||
const screen = screenRows[0];
|
||||
|
||||
const available = await isClientNameAvailable(pool, normalizedClientName, normalizedDeviceId, liveConnections);
|
||||
const available = await isClientNameAvailable(pool, normalizedClientName, null, liveConnections);
|
||||
if (!available) {
|
||||
const error = new Error('Client name already exists.');
|
||||
error.statusCode = 400;
|
||||
@@ -217,6 +241,38 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
const playerPublicUrl = String(options && options.playerPublicBaseUrl || process.env.PLAYER_PUBLIC_URL || process.env.PLAYER_BASE_URL || '').trim().replace(/\/$/, '');
|
||||
const bridgeBaseUrl = String(options && options.bridgeBaseUrl || process.env.BRIDGE_PUBLIC_URL || '').trim().replace(/\/$/, '');
|
||||
const playerDeviceId = normalizeDeviceId(options && options.playerDeviceId);
|
||||
const onPairingCode = options && typeof options.onPairingCode === 'function' ? options.onPairingCode : null;
|
||||
const pairingSessions = new Map();
|
||||
|
||||
function getPairingSession(deviceId, clientId) {
|
||||
const normalizedDeviceId = normalizeDeviceId(deviceId) || playerDeviceId;
|
||||
if (!normalizedDeviceId) {
|
||||
return null;
|
||||
}
|
||||
const sessionKey = `${normalizedDeviceId}:${normalizeDeviceId(clientId) || 'default'}`;
|
||||
let pairingSession = pairingSessions.get(sessionKey);
|
||||
if (!isValidOnboardingPairingCode(pairingSession, pairingSession && pairingSession.code)) {
|
||||
pairingSession = createPairingSession(normalizedDeviceId, clientId);
|
||||
pairingSessions.set(sessionKey, pairingSession);
|
||||
}
|
||||
if (onPairingCode) {
|
||||
onPairingCode(pairingSession.code, Array.from(pairingSessions.entries()).filter(function (entry) {
|
||||
return isValidOnboardingPairingCode(entry[1], entry[1] && entry[1].code);
|
||||
}).map(function (entry) {
|
||||
return { deviceId: entry[1].deviceId, clientId: entry[1].clientId || null, code: entry[1].code };
|
||||
}));
|
||||
}
|
||||
return pairingSession;
|
||||
}
|
||||
|
||||
function findPairingSession(pairingCode) {
|
||||
for (const [deviceId, pairingSession] of pairingSessions.entries()) {
|
||||
if (isValidOnboardingPairingCode(pairingSession, pairingCode)) {
|
||||
return { deviceId: pairingSession.deviceId, session: pairingSession };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!app || !common) {
|
||||
throw new Error('registerPlayerOnboardingRoutes requires app and common.');
|
||||
@@ -289,15 +345,71 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
next();
|
||||
}
|
||||
|
||||
app.get('/', function (_req, res) {
|
||||
function requireOnboardingAuth(req, res, next) {
|
||||
if (!sharedSecret) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const pageToken = String(req.headers['x-pulse-page-auth'] || '').trim();
|
||||
const pagePayload = verifyPageAuthToken(pageToken);
|
||||
if (pagePayload && String(pagePayload.scope || '').trim() === 'onboarding') {
|
||||
req.playerPageAuth = pagePayload;
|
||||
return next();
|
||||
}
|
||||
|
||||
if (verifyRequestAuth(req)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(401).json({ error: 'Onboarding authentication required.' });
|
||||
}
|
||||
|
||||
app.get('/', async function (req, res, next) {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
res.send(common.renderPlayerOnboardingLandingPage());
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.query && req.query.clientId);
|
||||
let status = null;
|
||||
if (deviceId) {
|
||||
if (bridgeBaseUrl) {
|
||||
const response = await fetchThinClient(req, '/api/onboarding/status?deviceId=' + encodeURIComponent(deviceId), {
|
||||
method: 'GET'
|
||||
});
|
||||
status = await readJsonResponse(response);
|
||||
} else {
|
||||
status = await getOnboardingStatus(pool, deviceId);
|
||||
}
|
||||
}
|
||||
const screenId = status && (status.screen_id || status.screenId);
|
||||
const screenSlug = status && (status.screen_slug || status.screenSlug);
|
||||
if (screenId && screenSlug) {
|
||||
return res.redirect('/screen/' + encodeURIComponent(screenSlug));
|
||||
}
|
||||
res.send(common.renderPlayerOnboardingLandingPage({ pairingCode: '' }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/onboard', async function (req, res, next) {
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
try {
|
||||
res.send(common.renderPlayerOnboardingFormPage(String(req.query.deviceId || playerDeviceId || '').trim()));
|
||||
const deviceId = playerDeviceId;
|
||||
const onboardingBaseUrl = String(process.env.WEB_PUBLIC_URL || '').trim().replace(/\/$/, '') || getPublicBaseUrl(req, playerPublicUrl);
|
||||
const clientId = normalizeDeviceId(req.query.clientId);
|
||||
const pairingSession = getPairingSession(deviceId, clientId);
|
||||
const pairingParams = [];
|
||||
if (pairingSession && pairingSession.code) {
|
||||
pairingParams.push(`code=${encodeURIComponent(pairingSession.code)}`);
|
||||
}
|
||||
const pairingQuery = pairingParams.length ? `?${pairingParams.join('&')}` : '';
|
||||
if (bridgeBaseUrl && pairingSession && pairingSession.code) {
|
||||
const response = await fetchThinClient(req, `/api/onboarding/url?pairingCode=${encodeURIComponent(pairingSession.code)}`);
|
||||
const payload = await readJsonResponse(response);
|
||||
if (payload && payload.url) {
|
||||
return res.redirect(String(payload.url));
|
||||
}
|
||||
}
|
||||
return res.redirect(`${onboardingBaseUrl}/pairing${pairingQuery}`);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@@ -315,7 +427,7 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
next();
|
||||
}, async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.query.deviceId) || playerDeviceId;
|
||||
const deviceId = normalizeDeviceId(req.query.deviceId || req.query.clientId);
|
||||
if (bridgeBaseUrl) {
|
||||
const response = await fetchThinClient(req, '/api/onboarding/status?deviceId=' + encodeURIComponent(deviceId || ''), {
|
||||
method: 'GET'
|
||||
@@ -347,6 +459,26 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/resolve', requireOnboardingAuth, function (req, res) {
|
||||
const pairingCode = String(req.query.pairingCode || '').trim();
|
||||
const pairing = findPairingSession(pairingCode);
|
||||
if (!pairing) {
|
||||
return res.status(401).json({ error: 'A valid kiosk pairing code is required.' });
|
||||
}
|
||||
res.json({ deviceId: pairing.deviceId, clientId: pairing.session.clientId || null });
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/session', requireOnboardingPageAuth, function (req, res) {
|
||||
const deviceId = normalizeDeviceId(req.query.deviceId) || playerDeviceId;
|
||||
const clientId = normalizeDeviceId(req.query.clientId);
|
||||
const pairingSession = getPairingSession(deviceId, clientId);
|
||||
if (!pairingSession) {
|
||||
return res.status(503).json({ error: 'Player identity is unavailable.' });
|
||||
}
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.json({ deviceId: deviceId, pairingCode: pairingSession.code });
|
||||
});
|
||||
|
||||
app.get('/api/onboarding/screens', requireOnboardingPageAuth, async function (_req, res, next) {
|
||||
try {
|
||||
if (bridgeBaseUrl) {
|
||||
@@ -366,14 +498,40 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.get('/api/onboarding/qr', async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.query.deviceId) || playerDeviceId;
|
||||
const clientId = normalizeDeviceId(req.query.clientId);
|
||||
if (!deviceId) {
|
||||
return res.status(400).json({ error: 'Device ID is required' });
|
||||
}
|
||||
const onboardingUrl = `${getPublicBaseUrl(req, playerPublicUrl)}/onboard?deviceId=${encodeURIComponent(deviceId)}`;
|
||||
const svg = await createStyledQrCodeSvg({ value: onboardingUrl, qr_margin: 20 });
|
||||
const pairingSession = getPairingSession(deviceId, clientId);
|
||||
if (!pairingSession) {
|
||||
return res.status(503).json({ error: 'Pairing session is unavailable.' });
|
||||
}
|
||||
if (bridgeBaseUrl) {
|
||||
const response = await fetchThinClient(req, `/api/onboarding/qr?pairingCode=${encodeURIComponent(pairingSession.code)}`);
|
||||
if (response && response.ok) {
|
||||
const svg = await response.text();
|
||||
res.set('Content-Type', response.headers.get('content-type') || 'image/svg+xml; charset=utf-8');
|
||||
res.set('Cache-Control', 'no-store');
|
||||
return res.send(svg);
|
||||
}
|
||||
}
|
||||
const onboardingBaseUrl = String(process.env.WEB_PUBLIC_URL || '').trim().replace(/\/$/, '') || getPublicBaseUrl(req, playerPublicUrl);
|
||||
const onboardingUrl = `${onboardingBaseUrl}/pairing?code=${encodeURIComponent(pairingSession.code)}`;
|
||||
const svg = await createStyledQrCodeSvg({
|
||||
value: onboardingUrl,
|
||||
qr_margin: 20,
|
||||
qr_dots_type: 'dots',
|
||||
qr_dots_color: '#f4f8f5',
|
||||
qr_corners_square_type: 'dot',
|
||||
qr_corners_square_color: '#f4f8f5',
|
||||
qr_corners_dot_type: 'dot',
|
||||
qr_corners_dot_color: '#f0bd70',
|
||||
qr_background_transparent: true
|
||||
});
|
||||
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.send(svg);
|
||||
@@ -382,20 +540,28 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/onboarding', requireOnboardingPageAuth, express.json(), async function (req, res, next) {
|
||||
app.post('/api/onboarding', express.json(), requireOnboardingAuth, async function (req, res, next) {
|
||||
try {
|
||||
const deviceId = normalizeDeviceId(req.body && req.body.deviceId) || playerDeviceId;
|
||||
const deviceId = playerDeviceId;
|
||||
const clientName = String((req.body && req.body.clientName) || '').trim();
|
||||
const screenSlug = String((req.body && req.body.screenSlug) || '').trim();
|
||||
const pairingCode = String((req.body && req.body.pairingCode) || '').trim();
|
||||
const clientId = normalizeDeviceId(req.body && req.body.clientId);
|
||||
const retryAfterSeconds = isOnboardingSignupRateLimited(req, deviceId);
|
||||
if (retryAfterSeconds) {
|
||||
res.set('Retry-After', String(retryAfterSeconds));
|
||||
return res.status(429).json({ error: 'Too many onboarding attempts. Please try again later.' });
|
||||
}
|
||||
|
||||
const pairing = findPairingSession(pairingCode);
|
||||
const pairingSession = pairing && pairing.session;
|
||||
if (!isValidOnboardingPairingCode(pairingSession, pairingCode)) {
|
||||
return res.status(401).json({ error: 'A valid kiosk pairing code is required.' });
|
||||
}
|
||||
|
||||
if (bridgeBaseUrl) {
|
||||
const forwardedBody = Object.assign({}, req.body || {}, {
|
||||
deviceId: deviceId
|
||||
clientId: clientId || null
|
||||
});
|
||||
const response = await fetch(new URL('/api/onboarding', bridgeBaseUrl).toString(), {
|
||||
method: 'POST',
|
||||
@@ -413,6 +579,9 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
if (!payload) {
|
||||
return res.status(502).json({ error: 'Player bridge returned an invalid response.' });
|
||||
}
|
||||
if (response.ok) {
|
||||
pairingSessions.delete(deviceId);
|
||||
}
|
||||
payload.playerUrl = payload && payload.screenSlug ? `${getPublicBaseUrl(req, playerPublicUrl)}/screen/${encodeURIComponent(payload.screenSlug)}` : `${getPublicBaseUrl(req, playerPublicUrl)}/screen/${encodeURIComponent(screenSlug)}`;
|
||||
return res.json(payload);
|
||||
}
|
||||
@@ -426,8 +595,11 @@ function registerPlayerOnboardingRoutes(app, options) {
|
||||
return res.status(400).json({ error: 'Screen is required' });
|
||||
}
|
||||
|
||||
const status = await bindDeviceToScreen(pool, deviceId, clientName, screenSlug, playerRuntime.isClientNameAvailableOnScreen, playerRuntime, onboardingStore);
|
||||
await bindPlayerToScreen(pool, deviceId, screenSlug);
|
||||
if (!clientId) {
|
||||
return res.status(400).json({ error: 'Client ID is required' });
|
||||
}
|
||||
const status = await bindDeviceToScreen(pool, clientId, clientName, screenSlug, playerRuntime.isClientNameAvailableOnScreen, playerRuntime, onboardingStore);
|
||||
pairingSessions.delete(deviceId);
|
||||
res.json({
|
||||
deviceId: deviceId,
|
||||
clientName: status ? status.client_name : clientName,
|
||||
@@ -456,5 +628,6 @@ module.exports = {
|
||||
upsertPlayerRegistration: upsertPlayerRegistration,
|
||||
bindPlayerToScreen: bindPlayerToScreen,
|
||||
bindDeviceToScreen: bindDeviceToScreen,
|
||||
isValidOnboardingPairingCode: isValidOnboardingPairingCode,
|
||||
registerPlayerOnboardingRoutes: registerPlayerOnboardingRoutes
|
||||
};
|
||||
@@ -65,10 +65,11 @@
|
||||
var formData = new FormData(form);
|
||||
var clientName = String(formData.get("clientName") || "").trim();
|
||||
var screenSlug = String(formData.get("screenSlug") || "").trim();
|
||||
var pairingCode = String(formData.get("pairingCode") || "").trim();
|
||||
if (!clientName) { setMessage("Client name is required."); return; }
|
||||
if (!screenSlug) { setMessage("Screen is required."); return; }
|
||||
setMessage("Saving client...");
|
||||
var payload = { clientName: clientName, screenSlug: screenSlug };
|
||||
var payload = { clientName: clientName, screenSlug: screenSlug, pairingCode: pairingCode };
|
||||
if (deviceId) {
|
||||
payload.deviceId = deviceId;
|
||||
}
|
||||
|
||||
@@ -13,23 +13,12 @@
|
||||
}
|
||||
var screenKey = "pulse-signage-player-screen-slug";
|
||||
var qr = document.getElementById("onboarding-qr");
|
||||
var status = document.getElementById("onboarding-status");
|
||||
var localForm = document.getElementById("onboarding-local-form");
|
||||
var localMessage = document.getElementById("onboarding-message");
|
||||
var localScreenSelect = document.getElementById("onboarding-screen-select");
|
||||
var pairingCodeElement = document.getElementById("onboarding-pairing-code");
|
||||
var qrPlaceholderSrc = "data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 320 320%22%3E%3Crect width=%22320%22 height=%22320%22 rx=%2224%22 fill=%22%23ffffff%22/%3E%3Crect x=%2230%22 y=%2230%22 width=%22260%22 height=%22260%22 rx=%2218%22 fill=%22%23f8fafc%22 stroke=%22%23cbd5e1%22 stroke-width=%223%22 stroke-dasharray=%2212 10%22/%3E%3Cpath d=%22M106 118h108M106 156h108M106 194h72%22 stroke=%22%2394a3b8%22 stroke-width=%2214%22 stroke-linecap=%22round%22/%3E%3Ccircle cx=%22128%22 cy=%22248%22 r=%2212%22 fill=%22%2394a3b8%22/%3E%3Ctext x=%22160%22 y=%2278%22 text-anchor=%22middle%22 fill=%22%230f172a%22 font-family=%22Arial,sans-serif%22 font-size=%2224%22 font-weight=%22700%22%3EQR code loading%3C/text%3E%3Ctext x=%22160%22 y=%22266%22 text-anchor=%22middle%22 fill=%22%234b5563%22 font-family=%22Arial,sans-serif%22 font-size=%2214%22%3EPlease wait%3C/text%3E%3C/svg%3E";
|
||||
function parseResponseError(response) {
|
||||
return response.text().then(function (text) {
|
||||
var fallbackMessage = text && text.trim() ? text.trim() : "Unable to save onboarding.";
|
||||
try {
|
||||
var payload = JSON.parse(text);
|
||||
return payload && payload.error ? payload.error : fallbackMessage;
|
||||
} catch (_error) {
|
||||
return fallbackMessage;
|
||||
}
|
||||
});
|
||||
}
|
||||
function getDeviceId() {
|
||||
var configuredDeviceId = document.getElementById("onboarding-shell");
|
||||
configuredDeviceId = configuredDeviceId ? String(configuredDeviceId.getAttribute("data-player-device-id") || "").trim() : "";
|
||||
if (configuredDeviceId) { return configuredDeviceId; }
|
||||
var stored = "";
|
||||
try { stored = window.sessionStorage.getItem(deviceKey) || ""; } catch (_error) { stored = ""; }
|
||||
if (stored) { return stored; }
|
||||
@@ -37,115 +26,57 @@
|
||||
try { window.sessionStorage.setItem(deviceKey, next); } catch (_error2) {}
|
||||
return next;
|
||||
}
|
||||
function setStatus(message) { if (status) { status.textContent = message; } }
|
||||
function setLocalMessage(message) { if (localMessage) { localMessage.textContent = message || ""; } }
|
||||
function setSelectOptions(select, screens, selectedSlug) {
|
||||
if (!select) { return; }
|
||||
while (select.firstChild) { select.removeChild(select.firstChild); }
|
||||
var placeholder = document.createElement("option");
|
||||
placeholder.value = "";
|
||||
placeholder.textContent = "Select a screen";
|
||||
select.appendChild(placeholder);
|
||||
(Array.isArray(screens) ? screens : []).forEach(function (screen) {
|
||||
var option = document.createElement("option");
|
||||
option.value = String(screen && screen.slug ? screen.slug : "");
|
||||
option.textContent = String(screen && (screen.name || screen.slug) ? (screen.name || screen.slug) : "Screen");
|
||||
if (selectedSlug && String(option.value) === String(selectedSlug)) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
function loadScreens(selectedSlug) {
|
||||
return fetch("/api/onboarding/screens", { cache: "no-store" })
|
||||
.then(function (response) { return response.ok ? response.json() : null; })
|
||||
.then(function (payload) {
|
||||
var screens = payload && Array.isArray(payload.screens) ? payload.screens : [];
|
||||
setSelectOptions(localScreenSelect, screens, selectedSlug);
|
||||
return screens;
|
||||
})
|
||||
.catch(function () { setSelectOptions(localScreenSelect, [], selectedSlug); return []; });
|
||||
}
|
||||
function loadQr(deviceId) {
|
||||
function loadQr(deviceId, clientId) {
|
||||
if (!qr) { return; }
|
||||
qr.onerror = function () {
|
||||
qr.src = qrPlaceholderSrc;
|
||||
};
|
||||
qr.src = "/api/onboarding/qr?deviceId=" + encodeURIComponent(deviceId);
|
||||
qr.src = "/api/onboarding/qr?deviceId=" + encodeURIComponent(deviceId) + "&clientId=" + encodeURIComponent(clientId);
|
||||
}
|
||||
function submitOnboarding(deviceId, clientName, screenSlug) {
|
||||
return fetch("/api/onboarding", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify({ deviceId: deviceId, clientName: clientName, screenSlug: screenSlug })
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return parseResponseError(response).then(function (messageText) {
|
||||
throw new Error(messageText);
|
||||
});
|
||||
})
|
||||
function loadPairingSession(deviceId, clientId) {
|
||||
return fetch("/api/onboarding/session?deviceId=" + encodeURIComponent(deviceId) + "&clientId=" + encodeURIComponent(clientId), { cache: "no-store" })
|
||||
.then(function (response) { return response.ok ? response.json() : null; })
|
||||
.then(function (payload) {
|
||||
if (!payload || !payload.screenSlug) { throw new Error("Unable to save onboarding."); }
|
||||
if (payload.clientName) { setSessionStorageItem(clientNameKey, payload.clientName); }
|
||||
if (payload.clientName && payload.screenSlug) { setSessionStorageItem(getClientNameStorageKey(payload.screenSlug), payload.clientName); }
|
||||
try { window.localStorage.setItem(screenKey, payload.screenSlug); } catch (_error) {}
|
||||
setLocalMessage("Onboarding complete.");
|
||||
if (localForm) {
|
||||
Array.prototype.slice.call(localForm.querySelectorAll("input, select, button")).forEach(function (control) {
|
||||
control.disabled = true;
|
||||
});
|
||||
if (payload && payload.pairingCode && pairingCodeElement) {
|
||||
pairingCodeElement.textContent = payload.pairingCode;
|
||||
}
|
||||
});
|
||||
return payload;
|
||||
})
|
||||
.catch(function () { return null; });
|
||||
}
|
||||
function getClientId() {
|
||||
var stored = getSessionStorageItem("pulse-signage-player-client-id");
|
||||
if (stored) { return stored; }
|
||||
var next = (window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : "client-" + Date.now() + "-" + Math.random().toString(16).slice(2));
|
||||
setSessionStorageItem("pulse-signage-player-client-id", next);
|
||||
return next;
|
||||
}
|
||||
function redirectIfOnboarded(deviceId) {
|
||||
return fetch("/api/onboarding/status?deviceId=" + encodeURIComponent(deviceId), { cache: "no-store" })
|
||||
var bindingId = getClientId() || deviceId;
|
||||
return fetch("/api/onboarding/status?deviceId=" + encodeURIComponent(bindingId), { cache: "no-store" })
|
||||
.then(function (response) { return response.ok ? response.json() : null; })
|
||||
.then(function (payload) {
|
||||
if (payload && payload.onboarded && payload.screenSlug) {
|
||||
if (payload.clientName) { setSessionStorageItem(clientNameKey, payload.clientName); }
|
||||
if (payload.clientName && payload.screenSlug) { setSessionStorageItem(getClientNameStorageKey(payload.screenSlug), payload.clientName); }
|
||||
try { window.localStorage.setItem(screenKey, payload.screenSlug); } catch (_error) {}
|
||||
window.location.replace("/screen/" + encodeURIComponent(payload.screenSlug));
|
||||
window.location.replace("/screen/" + encodeURIComponent(payload.screenSlug) + "?clientId=" + encodeURIComponent(clientId));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.catch(function () { return false; });
|
||||
}
|
||||
var clientId = getClientId();
|
||||
var deviceId = getDeviceId();
|
||||
if (localForm) {
|
||||
localForm.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
var formData = new FormData(localForm);
|
||||
var clientName = String(formData.get("clientName") || "").trim();
|
||||
var screenSlug = String(formData.get("screenSlug") || "").trim();
|
||||
if (!clientName) { setLocalMessage("Client name is required."); return; }
|
||||
if (!screenSlug) { setLocalMessage("Screen is required."); return; }
|
||||
setLocalMessage("Saving client...");
|
||||
submitOnboarding(deviceId, clientName, screenSlug).catch(function (error) {
|
||||
setLocalMessage(error && error.message ? error.message : "Unable to save onboarding.");
|
||||
});
|
||||
});
|
||||
}
|
||||
loadScreens().then(function () {
|
||||
try {
|
||||
var storedClientName = getSessionStorageItem(clientNameKey) || "";
|
||||
if (storedClientName && localForm) {
|
||||
var clientNameInput = localForm.querySelector("input[name=\"clientName\"]");
|
||||
if (clientNameInput) { clientNameInput.value = storedClientName; }
|
||||
}
|
||||
} catch (_error) {}
|
||||
});
|
||||
redirectIfOnboarded(deviceId).then(function (redirected) {
|
||||
if (redirected) { return; }
|
||||
if (qr && !qr.getAttribute("src")) {
|
||||
qr.src = qrPlaceholderSrc;
|
||||
}
|
||||
loadQr(deviceId);
|
||||
setStatus("Waiting for onboarding to finish.");
|
||||
loadPairingSession(deviceId, clientId).then(function () {
|
||||
loadQr(deviceId, clientId);
|
||||
});
|
||||
window.setInterval(function () { redirectIfOnboarded(deviceId); }, 2000);
|
||||
});
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user