openvibers/openvibe.live Commits
Live: the wallet pill catches up a channel's points on its page (#840)
Changes
@@@ -26,7 +26,7 @@"onnxruntime-node": "^1.20.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.408.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0","pg": "^8.23.1","sanitize-html": "^2.17.7","sdp-transform": "^3.0.0",@@@ -3417,9 +3417,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.211.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",-- "integrity": "sha512-3qMNLpByLNQPoCb9HGirVM5UTIPVYMLNshyhB185w1djaF3MLKnKahgwDpSlO3L8MAvhr+ZDzhyaedQdqTWcBQ==",++ "version": "4.212.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0",++ "integrity": "sha512-IwF+K4ZXJ25bNJkiwmC1niAv2QvzV9yzOQmI8lBmFT96Vr9N/RacWFCLmE/8myy0M+kXE/6HEdIW2+aVpK2Bgg==","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -46,7 +46,7 @@"onnxruntime-node": "^1.20.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.408.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0","pg": "^8.23.1","sanitize-html": "^2.17.7","sdp-transform": "^3.0.0",@@@ -964,6 +964,9 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionIif (isNewChannel) _popularAhead = { username: String(username).toLowerCase(), at: Date.now(), p: api(`/streams/channel/${encodeURIComponent(username)}/popular`).catch(() => null) };const data = await api(`/streams/channel/${username}?vodLimit=${CHANNEL_VODS_PAGE_SIZE}&vodOffset=${channelVodOffset}&clipLimit=${CHANNEL_CLIPS_PAGE_SIZE}&clipOffset=${channelClipOffset}${initialVodExtra}`);const ch = data.channel;++ // The wallet pill shows this channel's points once they are known (Shared 4.212.0, `purse.streamer`): the++ // channel-points ledger keys a streamer by the owner's numeric user id, ch.user_id, and only now is it here.++ if (ch && currentChannelUsername === username && window.LiveNav) window.LiveNav.setStreamer(ch.user_id);{ const page = document.getElementById('page-channel'); if (page) page.classList.add('ch-data'); if (page && !page.classList.contains('ch-ready')) _channelReadyTimer = setTimeout(_channelReady, 2500); }// The tab says whose channel this is, as the server-rendered <title> did (a live stream// replaces it with the stream's title in activateChannelStream).@@@ -1392,6 +1392,8 @@ function showPage(page) {if (link) link.classList.add('active');}if (window.LiveNav) window.LiveNav.setActive(navPage || null);++ // The wallet pill's channel-points context belongs to a channel page (Shared 4.212.0): no channel, no streamer.++ if (window.LiveNav && page !== 'channel') window.LiveNav.setStreamer(null);updateNavHeroTransparency();}@@@ -8,12 +8,19 @@* LiveNav.sync(user) after sign-in, sign-out or a profile change* LiveNav.setActive(page) the router calls this on every page change* LiveNav.setLive(isLive) the red dot on "Go Live"++ * LiveNav.setStreamer(id) a channel page's owner id, so the wallet pill catches up its channel points*/(function () {'use strict';const call = (name, ...a) => (typeof window[name] === 'function' ? window[name](...a) : undefined);const nav = () => window.OpenVibeNavbar;let booted = false;++ let current = null;++ // The purse's page context (Shared 4.212.0): a channel page passes its streamer's numeric user id, which is what the++ // channel-points ledger calls `streamer` (Live's channel_points.streamer_id, the owner of ch.user_id); every other++ // page passes nothing. The object is the same one on every render, so a channel that loads after the bar still++ // reaches the wallet's next catch-up without re-rendering the bar.++ const purse = {};// The bar's places, most used first (the owner, 2026-10-28: "move search after the Rundown and the drop-down… reanalyze// the nav bars… redundant options"). Search is the navbar's magnifier now (Shared 4.21: Live's results first, then all@@@ -77,6 +84,8 @@// The magnifier (Shared 4.21): Live's streams, recordings and clips first, Live's own search page, then OpenVibe.search: { owner: 'live', label: 'OpenVibe.Live', page: '/search' },wallet: true,++ // The wallet pill's page context: a channel page's streamer, so its points and their changes are caught up.++ purse,// Same-origin links are routes of this single-page app.onNavigate: (href, e) => (typeof handleLinkClick === 'function' ? handleLinkClick(e, href) : true),// No speculation rules: the router answers same-site links, so a prefetched page was never used, and the@@@ -92,8 +101,9 @@let last = { live: false, page: null };function sync(user) {++ current = user || null;const N = nav(); if (!N) return;-- booted = true; N.init(config(user)); // init() merges the new config and re-renders++ booted = true; N.init(config(current)); // init() merges the new config and re-rendersif (last.page) N.setActive(last.page);if (last.live) N.updateLink('broadcast', { dot: true });}@@@ -102,6 +112,17 @@sync,setActive(page) { last.page = page; const N = nav(); if (N && booted) N.setActive(page); },setLive(isLive) { last.live = !!isLive; const N = nav(); if (N && booted) N.updateLink('broadcast', { dot: !!isLive, label: isLive ? 'LIVE' : 'Go Live' }); },++ // A channel page's data carries its owner's numeric user id only after the page has loaded; from then on the++ // wallet's next catch-up asks for that channel's points too (Shared 4.212.0, /wallet/changes?streamer=). With++ // null — any other page — the purse asks for the person's own balances alone.++ setStreamer(id) {++ const n = Number(id);++ const next = Number.isSafeInteger(n) && n > 0 ? n : null;++ if (next === (purse.streamer || null)) return;++ if (next) purse.streamer = next; else delete purse.streamer;++ const N = nav();++ if (next && current && booted && N && typeof N.refreshWallet === 'function') N.refreshWallet();++ },};// First paint: the bar appears signed out immediately; app.js calls sync() again when it knows who is here.@@@ -3,9 +3,9 @@ const assert = require('assert');const fs = require('fs');const path = require('path');const vm = require('vm');--let config, opens = 0, refreshes = 0;++let config, opens = 0, refreshes = 0, inits = 0;const context = { window: { OpenVibeNavbar: {-- init(c) { config = c; }, setActive() {}, updateLink() {},++ init(c) { config = c; inits++; }, setActive() {}, updateLink() {},openWallet() { opens++; }, refreshWallet() { refreshes++; },} }, document: { readyState: 'loading', addEventListener() {} }, currentUser: null };vm.createContext(context);@@@ -13,6 +13,29 @@ vm.runInContext(fs.readFileSync(path.join(__dirname, '../public/js/ov-navbar-livcontext.window.LiveNav.sync({ id: 1, username: 'alice' });assert.strictEqual(config.wallet, true);assert.strictEqual(config.chips, undefined, 'Live does not override the shared balances');++// The purse (Shared 4.212.0, ask 273): on a channel page the wallet pill catches up that channel's points too. The++// streamer is the channel owner's numeric user id (app-channel.js reads ch.user_id after the channel data loads);++// elsewhere the bar passes nothing, so /wallet/changes carries no streamer.++assert.strictEqual(config.purse.streamer, undefined, 'no channel page: the purse gets no streamer');++context.window.LiveNav.setStreamer('alice');++assert.strictEqual(config.purse.streamer, undefined, 'a non-numeric id is not a streamer');++context.window.LiveNav.setStreamer(0);++assert.strictEqual(config.purse.streamer, undefined, 'zero is not a streamer');++context.window.LiveNav.setStreamer('42');++assert.strictEqual(config.purse.streamer, 42, 'the channel\'s numeric owner id reaches the purse');++assert.strictEqual(refreshes, 1, 'the wallet catches up at once instead of waiting for the 60s chip cache');++assert.strictEqual(inits, 1, 'the bar is not re-rendered for it');++context.window.LiveNav.setStreamer('42');++assert.strictEqual(refreshes, 1, 'the same streamer does not fetch twice');++context.window.LiveNav.setStreamer(null);++assert.strictEqual(config.purse.streamer, undefined, 'leaving the channel clears it');++assert.strictEqual(refreshes, 1, 'leaving a channel does not fetch again');++assert.strictEqual(config.purse.streamer, undefined, 'the purse is the same object, so the navbar sees the clear');++// The page wires it: app-channel.js after the channel data, app.js for every page that is not a channel.++const page = fs.readFileSync(path.join(__dirname, '../public/js/app-channel.js'), 'utf8');++assert.match(page, /const ch = data\.channel;\n[\s\S]{0,400}LiveNav\.setStreamer\(ch\.user_id\)/, 'a channel page passes ch.user_id once it loads');++const appSrc = fs.readFileSync(path.join(__dirname, '../public/js/app.js'), 'utf8');++assert.match(appSrc, /page !== 'channel'\) window\.LiveNav\.setStreamer\(null\)/, 'every other page passes no streamer');const wallet = config.menu.sections.flatMap((s) => s.items).find((i) => i.id === 'wallet');wallet.onClick(); assert.strictEqual(opens, 1);const root = path.join(__dirname, '..');@@@ -29,7 +52,6 @@ assert(!security.includes('"https://billing.openvibe.network"') && !security.inc// "refreshWallet is not a function" in onAuthChange for every signed-in person (2026-11-09, Live #717 on 4.168.0).const pin = /refs\/tags\/v(\d+)\.(\d+)\.(\d+)/.exec(JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).dependencies['openvibe-shared']);assert(pin && (Number(pin[1]) > 4 || (Number(pin[1]) === 4 && Number(pin[2]) >= 170)), 'openvibe-shared is pinned at 4.170.0 or later');--const app = fs.readFileSync(path.join(root, 'public/js/app.js'), 'utf8');--assert(/typeof window\.OpenVibeNavbar\.refreshWallet === 'function'\) window\.OpenVibeNavbar\.refreshWallet\(\)/.test(app),++assert(/typeof window\.OpenVibeNavbar\.refreshWallet === 'function'\) window\.OpenVibeNavbar\.refreshWallet\(\)/.test(appSrc),'sign-in never stops on a navbar without the wallet');console.log('Live delegates wallet and store to Shared, retaining cosmetic renderers and channel actions');
@@ -26,7 +26,7 @@
@@ -26,7 +26,7 @@
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.408.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.408.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0",
"pg": "^8.23.1",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
@@ -3417,9 +3417,9 @@
@@ -3417,9 +3417,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.211.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",
- "integrity": "sha512-3qMNLpByLNQPoCb9HGirVM5UTIPVYMLNshyhB185w1djaF3MLKnKahgwDpSlO3L8MAvhr+ZDzhyaedQdqTWcBQ==",
+ "version": "4.212.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0",
+ "integrity": "sha512-IwF+K4ZXJ25bNJkiwmC1niAv2QvzV9yzOQmI8lBmFT96Vr9N/RacWFCLmE/8myy0M+kXE/6HEdIW2+aVpK2Bgg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -46,7 +46,7 @@
@@ -46,7 +46,7 @@
"onnxruntime-node": "^1.20.0",
"onnxruntime-node": "^1.20.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.408.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.408.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.150.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.211.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.212.0",
"pg": "^8.23.1",
"pg": "^8.23.1",
"sanitize-html": "^2.17.7",
"sanitize-html": "^2.17.7",
"sdp-transform": "^3.0.0",
"sdp-transform": "^3.0.0",
@@ -964,6 +964,9 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
@@ -964,6 +964,9 @@ async function loadChannelPage(username, managedStreamRef = null, legacySessionI
if (isNewChannel) _popularAhead = { username: String(username).toLowerCase(), at: Date.now(), p: api(`/streams/channel/${encodeURIComponent(username)}/popular`).catch(() => null) };
if (isNewChannel) _popularAhead = { username: String(username).toLowerCase(), at: Date.now(), p: api(`/streams/channel/${encodeURIComponent(username)}/popular`).catch(() => null) };
const data = await api(`/streams/channel/${username}?vodLimit=${CHANNEL_VODS_PAGE_SIZE}&vodOffset=${channelVodOffset}&clipLimit=${CHANNEL_CLIPS_PAGE_SIZE}&clipOffset=${channelClipOffset}${initialVodExtra}`);
const data = await api(`/streams/channel/${username}?vodLimit=${CHANNEL_VODS_PAGE_SIZE}&vodOffset=${channelVodOffset}&clipLimit=${CHANNEL_CLIPS_PAGE_SIZE}&clipOffset=${channelClipOffset}${initialVodExtra}`);
const ch = data.channel;
const ch = data.channel;
+ // The wallet pill shows this channel's points once they are known (Shared 4.212.0, `purse.streamer`): the
+ // channel-points ledger keys a streamer by the owner's numeric user id, ch.user_id, and only now is it here.
+ if (ch && currentChannelUsername === username && window.LiveNav) window.LiveNav.setStreamer(ch.user_id);
{ const page = document.getElementById('page-channel'); if (page) page.classList.add('ch-data'); if (page && !page.classList.contains('ch-ready')) _channelReadyTimer = setTimeout(_channelReady, 2500); }
{ const page = document.getElementById('page-channel'); if (page) page.classList.add('ch-data'); if (page && !page.classList.contains('ch-ready')) _channelReadyTimer = setTimeout(_channelReady, 2500); }
// The tab says whose channel this is, as the server-rendered <title> did (a live stream
// The tab says whose channel this is, as the server-rendered <title> did (a live stream
// replaces it with the stream's title in activateChannelStream).
// replaces it with the stream's title in activateChannelStream).
@@ -1392,6 +1392,8 @@ function showPage(page) {
@@ -1392,6 +1392,8 @@ function showPage(page) {
if (link) link.classList.add('active');
if (link) link.classList.add('active');
}
}
if (window.LiveNav) window.LiveNav.setActive(navPage || null);
if (window.LiveNav) window.LiveNav.setActive(navPage || null);
+ // The wallet pill's channel-points context belongs to a channel page (Shared 4.212.0): no channel, no streamer.
+ if (window.LiveNav && page !== 'channel') window.LiveNav.setStreamer(null);
updateNavHeroTransparency();
updateNavHeroTransparency();
}
}
@@ -8,12 +8,19 @@
@@ -8,12 +8,19 @@
* LiveNav.sync(user) after sign-in, sign-out or a profile change
* LiveNav.sync(user) after sign-in, sign-out or a profile change
* LiveNav.setActive(page) the router calls this on every page change
* LiveNav.setActive(page) the router calls this on every page change
* LiveNav.setLive(isLive) the red dot on "Go Live"
* LiveNav.setLive(isLive) the red dot on "Go Live"
+ * LiveNav.setStreamer(id) a channel page's owner id, so the wallet pill catches up its channel points
*/
*/
(function () {
(function () {
'use strict';
'use strict';
const call = (name, ...a) => (typeof window[name] === 'function' ? window[name](...a) : undefined);
const call = (name, ...a) => (typeof window[name] === 'function' ? window[name](...a) : undefined);
const nav = () => window.OpenVibeNavbar;
const nav = () => window.OpenVibeNavbar;
let booted = false;
let booted = false;
+ let current = null;
+ // The purse's page context (Shared 4.212.0): a channel page passes its streamer's numeric user id, which is what the
+ // channel-points ledger calls `streamer` (Live's channel_points.streamer_id, the owner of ch.user_id); every other
+ // page passes nothing. The object is the same one on every render, so a channel that loads after the bar still
+ // reaches the wallet's next catch-up without re-rendering the bar.
+ const purse = {};
// The bar's places, most used first (the owner, 2026-10-28: "move search after the Rundown and the drop-down… reanalyze
// The bar's places, most used first (the owner, 2026-10-28: "move search after the Rundown and the drop-down… reanalyze
// the nav bars… redundant options"). Search is the navbar's magnifier now (Shared 4.21: Live's results first, then all
// the nav bars… redundant options"). Search is the navbar's magnifier now (Shared 4.21: Live's results first, then all
@@ -77,6 +84,8 @@
@@ -77,6 +84,8 @@
// The magnifier (Shared 4.21): Live's streams, recordings and clips first, Live's own search page, then OpenVibe.
// The magnifier (Shared 4.21): Live's streams, recordings and clips first, Live's own search page, then OpenVibe.
search: { owner: 'live', label: 'OpenVibe.Live', page: '/search' },
search: { owner: 'live', label: 'OpenVibe.Live', page: '/search' },
wallet: true,
wallet: true,
+ // The wallet pill's page context: a channel page's streamer, so its points and their changes are caught up.
+ purse,
// Same-origin links are routes of this single-page app.
// Same-origin links are routes of this single-page app.
onNavigate: (href, e) => (typeof handleLinkClick === 'function' ? handleLinkClick(e, href) : true),
onNavigate: (href, e) => (typeof handleLinkClick === 'function' ? handleLinkClick(e, href) : true),
// No speculation rules: the router answers same-site links, so a prefetched page was never used, and the
// No speculation rules: the router answers same-site links, so a prefetched page was never used, and the
@@ -92,8 +101,9 @@
@@ -92,8 +101,9 @@
let last = { live: false, page: null };
let last = { live: false, page: null };
function sync(user) {
function sync(user) {
+ current = user || null;
const N = nav(); if (!N) return;
const N = nav(); if (!N) return;
- booted = true; N.init(config(user)); // init() merges the new config and re-renders
+ booted = true; N.init(config(current)); // init() merges the new config and re-renders
if (last.page) N.setActive(last.page);
if (last.page) N.setActive(last.page);
if (last.live) N.updateLink('broadcast', { dot: true });
if (last.live) N.updateLink('broadcast', { dot: true });
}
}
@@ -102,6 +112,17 @@
@@ -102,6 +112,17 @@
sync,
sync,
setActive(page) { last.page = page; const N = nav(); if (N && booted) N.setActive(page); },
setActive(page) { last.page = page; const N = nav(); if (N && booted) N.setActive(page); },
setLive(isLive) { last.live = !!isLive; const N = nav(); if (N && booted) N.updateLink('broadcast', { dot: !!isLive, label: isLive ? 'LIVE' : 'Go Live' }); },
setLive(isLive) { last.live = !!isLive; const N = nav(); if (N && booted) N.updateLink('broadcast', { dot: !!isLive, label: isLive ? 'LIVE' : 'Go Live' }); },
+ // A channel page's data carries its owner's numeric user id only after the page has loaded; from then on the
+ // wallet's next catch-up asks for that channel's points too (Shared 4.212.0, /wallet/changes?streamer=). With
+ // null — any other page — the purse asks for the person's own balances alone.
+ setStreamer(id) {
+ const n = Number(id);
+ const next = Number.isSafeInteger(n) && n > 0 ? n : null;
+ if (next === (purse.streamer || null)) return;
+ if (next) purse.streamer = next; else delete purse.streamer;
+ const N = nav();
+ if (next && current && booted && N && typeof N.refreshWallet === 'function') N.refreshWallet();
+ },
};
};
// First paint: the bar appears signed out immediately; app.js calls sync() again when it knows who is here.
// First paint: the bar appears signed out immediately; app.js calls sync() again when it knows who is here.
@@ -3,9 +3,9 @@ const assert = require('assert');
@@ -3,9 +3,9 @@ const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const vm = require('vm');
const vm = require('vm');
-let config, opens = 0, refreshes = 0;
+let config, opens = 0, refreshes = 0, inits = 0;
const context = { window: { OpenVibeNavbar: {
const context = { window: { OpenVibeNavbar: {
- init(c) { config = c; }, setActive() {}, updateLink() {},
+ init(c) { config = c; inits++; }, setActive() {}, updateLink() {},
openWallet() { opens++; }, refreshWallet() { refreshes++; },
openWallet() { opens++; }, refreshWallet() { refreshes++; },
} }, document: { readyState: 'loading', addEventListener() {} }, currentUser: null };
} }, document: { readyState: 'loading', addEventListener() {} }, currentUser: null };
vm.createContext(context);
vm.createContext(context);
@@ -13,6 +13,29 @@ vm.runInContext(fs.readFileSync(path.join(__dirname, '../public/js/ov-navbar-liv
@@ -13,6 +13,29 @@ vm.runInContext(fs.readFileSync(path.join(__dirname, '../public/js/ov-navbar-liv
context.window.LiveNav.sync({ id: 1, username: 'alice' });
context.window.LiveNav.sync({ id: 1, username: 'alice' });
assert.strictEqual(config.wallet, true);
assert.strictEqual(config.wallet, true);
assert.strictEqual(config.chips, undefined, 'Live does not override the shared balances');
assert.strictEqual(config.chips, undefined, 'Live does not override the shared balances');
+// The purse (Shared 4.212.0, ask 273): on a channel page the wallet pill catches up that channel's points too. The
+// streamer is the channel owner's numeric user id (app-channel.js reads ch.user_id after the channel data loads);
+// elsewhere the bar passes nothing, so /wallet/changes carries no streamer.
+assert.strictEqual(config.purse.streamer, undefined, 'no channel page: the purse gets no streamer');
+context.window.LiveNav.setStreamer('alice');
+assert.strictEqual(config.purse.streamer, undefined, 'a non-numeric id is not a streamer');
+context.window.LiveNav.setStreamer(0);
+assert.strictEqual(config.purse.streamer, undefined, 'zero is not a streamer');
+context.window.LiveNav.setStreamer('42');
+assert.strictEqual(config.purse.streamer, 42, 'the channel\'s numeric owner id reaches the purse');
+assert.strictEqual(refreshes, 1, 'the wallet catches up at once instead of waiting for the 60s chip cache');
+assert.strictEqual(inits, 1, 'the bar is not re-rendered for it');
+context.window.LiveNav.setStreamer('42');
+assert.strictEqual(refreshes, 1, 'the same streamer does not fetch twice');
+context.window.LiveNav.setStreamer(null);
+assert.strictEqual(config.purse.streamer, undefined, 'leaving the channel clears it');
+assert.strictEqual(refreshes, 1, 'leaving a channel does not fetch again');
+assert.strictEqual(config.purse.streamer, undefined, 'the purse is the same object, so the navbar sees the clear');
+// The page wires it: app-channel.js after the channel data, app.js for every page that is not a channel.
+const page = fs.readFileSync(path.join(__dirname, '../public/js/app-channel.js'), 'utf8');
+assert.match(page, /const ch = data\.channel;\n[\s\S]{0,400}LiveNav\.setStreamer\(ch\.user_id\)/, 'a channel page passes ch.user_id once it loads');
+const appSrc = fs.readFileSync(path.join(__dirname, '../public/js/app.js'), 'utf8');
+assert.match(appSrc, /page !== 'channel'\) window\.LiveNav\.setStreamer\(null\)/, 'every other page passes no streamer');
const wallet = config.menu.sections.flatMap((s) => s.items).find((i) => i.id === 'wallet');
const wallet = config.menu.sections.flatMap((s) => s.items).find((i) => i.id === 'wallet');
wallet.onClick(); assert.strictEqual(opens, 1);
wallet.onClick(); assert.strictEqual(opens, 1);
const root = path.join(__dirname, '..');
const root = path.join(__dirname, '..');
@@ -29,7 +52,6 @@ assert(!security.includes('"https://billing.openvibe.network"') && !security.inc
@@ -29,7 +52,6 @@ assert(!security.includes('"https://billing.openvibe.network"') && !security.inc
// "refreshWallet is not a function" in onAuthChange for every signed-in person (2026-11-09, Live #717 on 4.168.0).
// "refreshWallet is not a function" in onAuthChange for every signed-in person (2026-11-09, Live #717 on 4.168.0).
const pin = /refs\/tags\/v(\d+)\.(\d+)\.(\d+)/.exec(JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).dependencies['openvibe-shared']);
const pin = /refs\/tags\/v(\d+)\.(\d+)\.(\d+)/.exec(JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).dependencies['openvibe-shared']);
assert(pin && (Number(pin[1]) > 4 || (Number(pin[1]) === 4 && Number(pin[2]) >= 170)), 'openvibe-shared is pinned at 4.170.0 or later');
assert(pin && (Number(pin[1]) > 4 || (Number(pin[1]) === 4 && Number(pin[2]) >= 170)), 'openvibe-shared is pinned at 4.170.0 or later');
-const app = fs.readFileSync(path.join(root, 'public/js/app.js'), 'utf8');
-assert(/typeof window\.OpenVibeNavbar\.refreshWallet === 'function'\) window\.OpenVibeNavbar\.refreshWallet\(\)/.test(app),
+assert(/typeof window\.OpenVibeNavbar\.refreshWallet === 'function'\) window\.OpenVibeNavbar\.refreshWallet\(\)/.test(appSrc),
'sign-in never stops on a navbar without the wallet');
'sign-in never stops on a navbar without the wallet');
console.log('Live delegates wallet and store to Shared, retaining cosmetic renderers and channel actions');
console.log('Live delegates wallet and store to Shared, retaining cosmetic renderers and channel actions');