Show Viaplay subscription type on each account after add/check.

Auto-probe on account add and always render an Abonnement row, with session-cookie fallback when overview is unavailable.
This commit is contained in:
Jos Vooges | STH 2026-09-22 22:37:40 +02:00
parent 7cab13c5a8
commit 8be634f783
3 changed files with 139 additions and 31 deletions

View file

@ -143,13 +143,22 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
});
const d = await r.json();
if (!r.ok) throw new Error(d.error?.message ?? "Account toevoegen mislukt");
const emailKey = newEmail.trim().toLowerCase();
applySettings(d);
setNewLabel("");
setNewEmail("");
setNewSession("");
setNewAccessToken("");
setNewProfileId("");
setMsg("Viaplay-account toegevoegd");
const added = (d.accounts as ViaplayAccount[] | undefined)?.find(
(a) => a.email === emailKey
);
const sub = added?.subscriptionSummary;
setMsg(
sub
? `Account toegevoegd · abonnement: ${sub}`
: "Account toegevoegd — abonnement nog niet zichtbaar, druk Check"
);
} catch (e) {
setErr(String((e as Error).message ?? e));
} finally {
@ -378,31 +387,43 @@ export function ViaplayPanel({ onChanged }: { onChanged?: (o: ViaplayOverview) =
borderBottom: "1px solid var(--border, #333)",
}}
>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
<strong>{a.label || a.email}</strong>
{healthBadge(a)}
<span className="muted" style={{ fontSize: "0.85rem" }}>
{a.email}
{" · "}
{a.hasSession && a.hasAccessToken ? "tokens ok" : "tokens missen"}
{a.eventCount ? ` · ${a.eventCount} events` : ""}
</span>
{(a.subscriptionSummary || a.partnerName || a.healthError) && (
<div style={{ width: "100%", fontSize: "0.85rem", marginTop: 2 }}>
<div style={{ flex: "1 1 280px", minWidth: 0 }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
<strong>{a.label || a.email}</strong>
{healthBadge(a)}
<span className="muted" style={{ fontSize: "0.85rem" }}>
{a.email}
{a.eventCount ? ` · ${a.eventCount} events` : ""}
</span>
</div>
<div
style={{
marginTop: 8,
padding: "8px 10px",
borderRadius: 6,
background: "var(--surface-2, rgba(127,127,127,0.12))",
fontSize: "0.9rem",
}}
>
<div>
<span className="muted">Abonnement: </span>
{a.subscriptionSummary ? (
<span className="ok-banner" style={{ display: "inline", padding: "2px 8px" }}>
{a.subscriptionSummary}
</span>
) : a.partnerName ? (
<span className="muted">{a.partnerName}</span>
) : null}
{a.healthError ? (
<span className="muted" style={{ marginLeft: 8 }} title={a.healthError}>
{a.healthError}
</span>
) : null}
<strong>{a.subscriptionSummary}</strong>
) : (
<span className="muted">nog niet opgehaald — druk Check</span>
)}
</div>
)}
{a.partnerName && a.subscriptionSummary && !a.subscriptionSummary.includes(a.partnerName) ? (
<div className="muted" style={{ fontSize: "0.8rem", marginTop: 2 }}>
Partner: {a.partnerName}
</div>
) : null}
{a.healthError ? (
<div className="muted" style={{ fontSize: "0.8rem", marginTop: 4 }}>
{a.healthError}
</div>
) : null}
</div>
</div>
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
<button type="button" disabled={busy} onClick={() => void probeAccounts(a.id)}>

View file

@ -115,7 +115,22 @@ export async function fetchAccountOverview(
lastStatus = res.status;
const body = asRecord(res.json);
if (res.status === 200 && body) {
return parseOverviewBody(body);
const parsed = parseOverviewBody(body);
if (!parsed.productNames.length || !parsed.partnerName) {
const fromSession = subscriptionFromSession(creds.session);
const partnerName = parsed.partnerName || fromSession.partnerName;
const productNames = parsed.productNames.length
? parsed.productNames
: fromSession.productNames;
const parts = [partnerName, productNames.join(", ")].filter(Boolean);
return {
...parsed,
partnerName,
productNames,
summary: parts.join(" · ") || parsed.summary,
};
}
return parsed;
}
lastError =
(typeof body?.message === "string" && body.message) ||
@ -141,21 +156,37 @@ export async function fetchAccountOverview(
asRecord(root?._links)?.["viaplay:logout"]
);
if (ping.status === 200 && hasUser) {
const fromSession = subscriptionFromSession(creds.session);
const exp = accessTokenExpiry(creds.accessToken);
const summary =
fromSession.summary ||
(exp
? `Sessie actief · token tot ${exp}`
: "Sessie actief (abonnementdetails niet beschikbaar)");
return {
ok: true,
status: ping.status,
partnerName: null,
productNames: [],
summary: exp
? `Sessie actief · token tot ${exp}`
: "Sessie actief (abonnementdetails niet beschikbaar)",
partnerName: fromSession.partnerName,
productNames: fromSession.productNames,
summary,
};
}
} catch {
/* ignore */
}
// Laatste redmiddel: abonnement uit session-cookie (lokaal, geen extra call)
const fromSession = subscriptionFromSession(creds.session);
if (fromSession.summary) {
return {
ok: true,
status: lastStatus || 200,
partnerName: fromSession.partnerName,
productNames: fromSession.productNames,
summary: fromSession.summary,
};
}
return {
ok: false,
status: lastStatus,
@ -166,6 +197,52 @@ export async function fetchAccountOverview(
};
}
/** Haal partner/pakket uit de Viaplay session-cookie (zonder netwerk). */
export function subscriptionFromSession(session: string | null | undefined): {
partnerName: string | null;
productNames: string[];
summary: string | null;
} {
if (!session?.trim()) {
return { partnerName: null, productNames: [], summary: null };
}
try {
let raw = session.trim();
try {
raw = decodeURIComponent(raw);
} catch {
/* already decoded */
}
// express-session vorm: s:j:{json}.{signature}
const m = /^s:j:(.+)\.[^.]+$/s.exec(raw);
const jsonText = m?.[1] ?? null;
if (!jsonText) return { partnerName: null, productNames: [], summary: null };
const data = JSON.parse(jsonText) as Record<string, unknown>;
const contracts = String(data.vmsContracts ?? "").trim();
const productNames: string[] = [];
let partnerName: string | null = null;
if (/tmobile|odido/i.test(contracts) || data.isVmsCustomer === true) {
partnerName = "Odido Internet + TV";
}
if (/b2b_vtotal|vtotal/i.test(contracts)) productNames.push("Total / V Total");
else if (/basis|basic/i.test(contracts)) productNames.push("Basis");
else if (contracts) {
productNames.push(contracts.replace(/\s+/g, " "));
}
if (data.isTveBCustomer === true && !productNames.length) {
productNames.push("TV-pakket");
}
const parts = [partnerName, productNames.join(", ")].filter(Boolean);
return {
partnerName,
productNames,
summary: parts.length ? parts.join(" · ") : null,
};
} catch {
return { partnerName: null, productNames: [], summary: null };
}
}
function accessTokenExpiry(token: string | null): string | null {
if (!token) return null;
try {

View file

@ -216,7 +216,8 @@ export async function addViaplayAccount(
}
config.accounts.push(account);
await persist(config, existing?.enabled ?? true);
return getViaplaySettingsPublic();
// Direct abonnement ophalen zodat UI het meteen toont
return probeViaplayAccounts(sessionSecret, account.id);
}
export async function updateViaplayAccount(
@ -286,6 +287,15 @@ export async function updateViaplayAccount(
: current.passwordEnc,
};
await persist(config, existing?.enabled ?? true);
const tokensChanged = !!(
input.session?.trim() ||
input.accessToken?.trim() ||
input.profileId !== undefined ||
input.clearCookies
);
if (tokensChanged) {
return probeViaplayAccounts(sessionSecret, accountId);
}
return getViaplaySettingsPublic();
}