";
}
- function createTeamControlCell(className) {
+ function createAccessControlCell(className) {
var cell = document.createElement("div");
- cell.className = "team-control-cell " + className;
+ cell.className = "access-control-cell " + className;
return cell;
}
- function renderTeamRoleControl(accountID, member, isOwner) {
- var currentRole = normalizedTeamRole(member.role);
- var group = createTeamControlCell("team-control-cell-role");
+ function renderAccessRoleControl(accountID, member, isOwner) {
+ var currentRole = normalizedAccessRole(member.role);
+ var group = createAccessControlCell("access-control-cell-role");
if (currentRole === "owner" && !isOwner) {
var locked = document.createElement("span");
- locked.className = "team-role-badge";
+ locked.className = "access-role-badge";
locked.textContent = roleLabel(currentRole);
group.appendChild(locked);
return group;
}
var sel = document.createElement("select");
- sel.className = "team-role-select";
+ sel.className = "access-role-select";
var roles = isOwner ? ["owner", "admin", "tech", "read_only"] : ["admin", "tech", "read_only"];
for (var j = 0; j < roles.length; j += 1) {
var opt = document.createElement("option");
@@ -225,11 +225,11 @@
group.appendChild(sel);
return group;
}
- function renderTeamMemberAction(accountID, member, isOwner) {
- if (normalizedTeamRole(member.role) === "owner" && !isOwner) {
- var locked = createTeamControlCell("team-control-cell-access");
+ function renderAccessMemberAction(accountID, member, isOwner) {
+ if (normalizedAccessRole(member.role) === "owner" && !isOwner) {
+ var locked = createAccessControlCell("access-control-cell-access");
var lockedText = document.createElement("span");
- lockedText.className = "team-control-locked";
+ lockedText.className = "access-control-locked";
lockedText.textContent = "Locked";
locked.appendChild(lockedText);
return locked;
@@ -242,40 +242,40 @@
btn.setAttribute("data-account-id", accountID);
btn.setAttribute("data-user-id", member.user_id);
btn.setAttribute("data-member-email", member.email);
- var group = createTeamControlCell("team-control-cell-access");
+ var group = createAccessControlCell("access-control-cell-access");
group.appendChild(btn);
return group;
}
- function renderTeamMemberRow(accountID, member, isOwner) {
+ function renderAccessMemberRow(accountID, member, isOwner) {
var row = document.createElement("div");
- row.className = "team-member-row";
+ row.className = "access-member-row";
var identity = document.createElement("div");
- identity.className = "team-member-identity";
+ identity.className = "access-member-identity";
var topline = document.createElement("div");
- topline.className = "team-member-topline";
+ topline.className = "access-member-topline";
var email = document.createElement("div");
- email.className = "team-member-email";
+ email.className = "access-member-email";
email.textContent = member.email;
topline.appendChild(email);
var roleBadge = document.createElement("span");
- roleBadge.className = "team-inline-role-badge";
+ roleBadge.className = "access-inline-role-badge";
roleBadge.textContent = roleLabel(member.role);
topline.appendChild(roleBadge);
identity.appendChild(topline);
var caption = document.createElement("div");
- caption.className = "team-member-caption";
+ caption.className = "access-member-caption";
caption.textContent = roleCapabilityCopy(member.role);
identity.appendChild(caption);
row.appendChild(identity);
- row.appendChild(renderTeamRoleControl(accountID, member, isOwner));
- row.appendChild(renderTeamMemberAction(accountID, member, isOwner) || createTeamControlCell("team-control-cell-access"));
+ row.appendChild(renderAccessRoleControl(accountID, member, isOwner));
+ row.appendChild(renderAccessMemberAction(accountID, member, isOwner) || createAccessControlCell("access-control-cell-access"));
return row;
}
- function ensureRosterHead(container) {
- var existing = container.querySelector(".team-roster-head");
+ function ensureAccessRosterHead(container) {
+ var existing = container.querySelector(".access-roster-head");
if (existing) return;
var head = document.createElement("div");
- head.className = "team-roster-head";
+ head.className = "access-roster-head";
head.innerHTML = "OperatorRoleAccess";
container.appendChild(head);
}
@@ -288,29 +288,29 @@
spinner.hidden = !entry.createWorkspace.pending;
}
}
- function renderTeamSection(accountID, entry) {
- var section = getElement("team-section-" + accountID);
- var roster = getElement("team-list-" + accountID);
+ function renderAccessSection(accountID, entry) {
+ var section = getElement("access-section-" + accountID);
+ var roster = getElement("access-list-" + accountID);
if (!section || !roster) return;
- var rosterPanel = roster.closest(".team-roster");
+ var rosterPanel = roster.closest(".access-roster");
var actorRole = section.getAttribute("data-actor-role") || "";
var isOwner = actorRole === "owner";
- section.classList.toggle("visible", entry.teamVisible);
- renderTeamStats(accountID, entry);
- if (!entry.teamVisible) {
+ section.classList.toggle("visible", entry.accessVisible);
+ renderAccessStats(accountID, entry);
+ if (!entry.accessVisible) {
return;
}
- if (entry.teamQuery.status === "loading") {
+ if (entry.accessQuery.status === "loading") {
if (rosterPanel) rosterPanel.classList.add("state-only");
setContainerMessage(roster, "Loading roster", "Checking who currently has access to this account.", false);
return;
}
- if (entry.teamQuery.status === "error") {
+ if (entry.accessQuery.status === "error") {
if (rosterPanel) rosterPanel.classList.add("state-only");
- setContainerMessage(roster, "Roster needs attention", entry.teamQuery.error, true);
+ setContainerMessage(roster, "Roster needs attention", entry.accessQuery.error, true);
return;
}
- if (!entry.teamQuery.data.length) {
+ if (!entry.accessQuery.data.length) {
if (rosterPanel) rosterPanel.classList.add("state-only");
setContainerMessage(roster, "No one added yet", "Invite someone new when this account needs shared access.", false);
return;
@@ -318,10 +318,10 @@
roster.textContent = "";
roster.classList.remove("state-only");
if (rosterPanel) rosterPanel.classList.remove("state-only");
- ensureRosterHead(roster);
- for (var i = 0; i < entry.teamQuery.data.length; i += 1) {
- var member = entry.teamQuery.data[i];
- roster.appendChild(renderTeamMemberRow(accountID, member, isOwner));
+ ensureAccessRosterHead(roster);
+ for (var i = 0; i < entry.accessQuery.data.length; i += 1) {
+ var member = entry.accessQuery.data[i];
+ roster.appendChild(renderAccessMemberRow(accountID, member, isOwner));
}
}
function renderAccountUI(accountState, accounts) {
@@ -338,7 +338,7 @@
}
renderAddWorkspaceSection(accountID, entry);
if (account) renderWorkspaceManagement(account, entry);
- renderTeamSection(accountID, entry);
+ renderAccessSection(accountID, entry);
}
}
@@ -361,10 +361,10 @@
event.preventDefault();
void deps.runtime.openBilling(accountID);
return;
- case "toggle-team":
+ case "show-access":
event.preventDefault();
deps.setShellSection("access");
- deps.runtime.ensureTeamVisible(accountID);
+ deps.runtime.ensureAccessVisible(accountID);
return;
case "invite-member":
event.preventDefault();
@@ -574,7 +574,7 @@
}, "Failed to open billing portal.");
},
listMembers: function(accountID) {
- return request(accountURL(accountID, "/members"), {}, "Failed to load team.");
+ return request(accountURL(accountID, "/members"), {}, "Failed to load access roster.");
},
inviteMember: function(accountID, body) {
return request(accountURL(accountID, "/members"), {
@@ -657,15 +657,15 @@
createWorkspace: createMutationState(),
selectedWorkspaceID: "",
manageWorkspace: createMutationState(),
- teamVisible: false,
- teamQuery: createQueryState([])
+ accessVisible: false,
+ accessQuery: createQueryState([])
};
}
return accountState.byAccountID[accountID];
}
- function createPortalServiceState() {
+ function createPortalBillingState() {
return {
- openPanelID: "",
+ openBillingPanelID: "",
flows: {
manage: newVerificationFlowState(),
retrieve: newVerificationFlowState(),
@@ -685,76 +685,76 @@
loginState.emailValue = email || "";
}
}
- function syncServiceStateBootstrapEmail(serviceState, email) {
- if (!serviceState.flows.manage.emailValue) serviceState.flows.manage.emailValue = email || "";
- if (!serviceState.flows.retrieve.emailValue) serviceState.flows.retrieve.emailValue = email || "";
- if (!serviceState.flows.export.emailValue) serviceState.flows.export.emailValue = email || "";
- if (!serviceState.flows.delete.emailValue) serviceState.flows.delete.emailValue = email || "";
- if (!serviceState.refund.emailValue) serviceState.refund.emailValue = email || "";
+ function syncBillingStateBootstrapEmail(billingState, email) {
+ if (!billingState.flows.manage.emailValue) billingState.flows.manage.emailValue = email || "";
+ if (!billingState.flows.retrieve.emailValue) billingState.flows.retrieve.emailValue = email || "";
+ if (!billingState.flows.export.emailValue) billingState.flows.export.emailValue = email || "";
+ if (!billingState.flows.delete.emailValue) billingState.flows.delete.emailValue = email || "";
+ if (!billingState.refund.emailValue) billingState.refund.emailValue = email || "";
}
- function setFlowStatus(serviceState, flowID, message, isError) {
- serviceState.flows[flowID].status = {
+ function setFlowStatus(billingState, flowID, message, isError) {
+ billingState.flows[flowID].status = {
visible: true,
message,
error: !!isError
};
}
- function clearFlowStatus(serviceState, flowID) {
- serviceState.flows[flowID].status = emptyStatus();
+ function clearFlowStatus(billingState, flowID) {
+ billingState.flows[flowID].status = emptyStatus();
}
- function setRefundStatus(serviceState, message, isError) {
- serviceState.refund.status = {
+ function setRefundStatus(billingState, message, isError) {
+ billingState.refund.status = {
visible: true,
message,
error: !!isError
};
}
- function toggleServicePanelState(serviceState, panelID) {
- serviceState.openPanelID = serviceState.openPanelID === panelID ? "" : panelID;
+ function toggleBillingPanelState(billingState, panelID) {
+ billingState.openBillingPanelID = billingState.openBillingPanelID === panelID ? "" : panelID;
}
- function resetVerificationFlowState(serviceState, flowID) {
- var previous = serviceState.flows[flowID];
- serviceState.flows[flowID] = newVerificationFlowState();
- serviceState.flows[flowID].emailValue = previous.emailValue;
+ function resetVerificationFlowState(billingState, flowID) {
+ var previous = billingState.flows[flowID];
+ billingState.flows[flowID] = newVerificationFlowState();
+ billingState.flows[flowID].emailValue = previous.emailValue;
}
- function updateServiceInputValue(serviceState, inputKind, value) {
+ function updateBillingInputValue(billingState, inputKind, value) {
switch (inputKind) {
case "manage-email":
- serviceState.flows.manage.emailValue = value;
+ billingState.flows.manage.emailValue = value;
return;
case "manage-code":
- serviceState.flows.manage.codeValue = value;
+ billingState.flows.manage.codeValue = value;
return;
case "retrieve-email":
- serviceState.flows.retrieve.emailValue = value;
+ billingState.flows.retrieve.emailValue = value;
return;
case "retrieve-code":
- serviceState.flows.retrieve.codeValue = value;
+ billingState.flows.retrieve.codeValue = value;
return;
case "refund-email":
- serviceState.refund.emailValue = value;
+ billingState.refund.emailValue = value;
return;
case "refund-token":
- serviceState.refund.tokenValue = value;
+ billingState.refund.tokenValue = value;
return;
case "data-export-email":
- serviceState.flows.export.emailValue = value;
+ billingState.flows.export.emailValue = value;
return;
case "data-export-code":
- serviceState.flows.export.codeValue = value;
+ billingState.flows.export.codeValue = value;
return;
case "data-delete-email":
- serviceState.flows.delete.emailValue = value;
+ billingState.flows.delete.emailValue = value;
return;
case "data-delete-code":
- serviceState.flows.delete.codeValue = value;
+ billingState.flows.delete.codeValue = value;
return;
default:
return;
}
}
- function updateDeleteConfirmation(serviceState, checked) {
- serviceState.flows.delete.checkboxChecked = checked;
+ function updateDeleteConfirmation(billingState, checked) {
+ billingState.flows.delete.checkboxChecked = checked;
}
// src/account_runtime.ts
@@ -772,40 +772,40 @@
var renderAccountRuntime = function() {
renderAccountUI(deps.store.getAccountState(), deps.store.getBootstrap().accounts || []);
};
- var loadTeam = async function(accountID) {
- var section = getElement("team-section-" + accountID);
+ var loadAccessRoster = async function(accountID) {
+ var section = getElement("access-section-" + accountID);
if (!section) return;
deps.store.updateAccountState(function(accountState) {
var entry = ensurePortalAccountUIEntry(accountState, accountID);
- entry.teamVisible = true;
- beginQueryState(entry.teamQuery, []);
+ entry.accessVisible = true;
+ beginQueryState(entry.accessQuery, []);
});
try {
var members = await deps.api.listMembers(accountID);
deps.store.updateAccountState(function(accountState) {
var entry = ensurePortalAccountUIEntry(accountState, accountID);
- resolveQueryState(entry.teamQuery, Array.isArray(members) ? members : []);
+ resolveQueryState(entry.accessQuery, Array.isArray(members) ? members : []);
});
} catch (error) {
deps.store.updateAccountState(function(accountState) {
var entry = ensurePortalAccountUIEntry(accountState, accountID);
- failQueryState(entry.teamQuery, [], error instanceof Error ? error.message : "Network error.");
+ failQueryState(entry.accessQuery, [], error instanceof Error ? error.message : "Network error.");
});
}
};
- var refreshAccountTeamSection = async function(accountID) {
+ var refreshAccountAccessSection = async function(accountID) {
if (!await refreshOrRedirect()) {
return false;
}
- var section = getElement("team-section-" + accountID);
+ var section = getElement("access-section-" + accountID);
if (!section) {
return true;
}
deps.store.updateAccountState(function(accountState) {
var entry = ensurePortalAccountUIEntry(accountState, accountID);
- entry.teamVisible = true;
+ entry.accessVisible = true;
});
- await loadTeam(accountID);
+ await loadAccessRoster(accountID);
return true;
};
var toggleAddWorkspace = function(accountID) {
@@ -814,7 +814,7 @@
var entry = ensurePortalAccountUIEntry(accountState, accountID);
entry.addWorkspaceOpen = !entry.addWorkspaceOpen;
if (entry.addWorkspaceOpen) {
- entry.teamVisible = false;
+ entry.accessVisible = false;
entry.selectedWorkspaceID = "";
}
shouldFocus = entry.addWorkspaceOpen;
@@ -828,7 +828,7 @@
var entry = ensurePortalAccountUIEntry(accountState, accountID);
entry.selectedWorkspaceID = entry.selectedWorkspaceID === workspaceID ? "" : workspaceID;
if (entry.selectedWorkspaceID) {
- entry.teamVisible = false;
+ entry.accessVisible = false;
entry.addWorkspaceOpen = false;
}
});
@@ -922,34 +922,34 @@
deps.showToast(error instanceof Error ? error.message : "Failed to open billing portal.", true);
}
};
- var toggleTeam = function(accountID) {
+ var toggleAccess = function(accountID) {
var nextVisible = false;
deps.store.updateAccountState(function(accountState) {
var entry = ensurePortalAccountUIEntry(accountState, accountID);
- entry.teamVisible = !entry.teamVisible;
- if (entry.teamVisible) {
+ entry.accessVisible = !entry.accessVisible;
+ if (entry.accessVisible) {
entry.selectedWorkspaceID = "";
entry.addWorkspaceOpen = false;
}
- nextVisible = entry.teamVisible;
+ nextVisible = entry.accessVisible;
});
if (nextVisible) {
- void loadTeam(accountID);
+ void loadAccessRoster(accountID);
}
};
- var ensureTeamVisible = function(accountID) {
+ var ensureAccessVisible = function(accountID) {
var shouldLoad = false;
deps.store.updateAccountState(function(accountState) {
var entry = ensurePortalAccountUIEntry(accountState, accountID);
- if (!entry.teamVisible) {
- entry.teamVisible = true;
+ if (!entry.accessVisible) {
+ entry.accessVisible = true;
entry.selectedWorkspaceID = "";
entry.addWorkspaceOpen = false;
}
- shouldLoad = entry.teamQuery.status === "idle" || entry.teamQuery.status === "error";
+ shouldLoad = entry.accessQuery.status === "idle" || entry.accessQuery.status === "error";
});
if (shouldLoad) {
- void loadTeam(accountID);
+ void loadAccessRoster(accountID);
}
};
var inviteMember = async function(accountID) {
@@ -964,7 +964,7 @@
try {
await deps.api.inviteMember(accountID, { email, role: roleEl.value });
emailEl.value = "";
- if (!await refreshAccountTeamSection(accountID)) {
+ if (!await refreshAccountAccessSection(accountID)) {
return;
}
deps.showToast("Member invited!");
@@ -979,25 +979,25 @@
var changeRole = async function(accountID, userID, newRole) {
try {
await deps.api.updateMemberRole(accountID, userID, { role: newRole });
- if (!await refreshAccountTeamSection(accountID)) {
+ if (!await refreshAccountAccessSection(accountID)) {
return;
}
deps.showToast("Role updated.");
} catch (error) {
if (error instanceof PortalAPIError && error.status === 409) {
deps.showToast("Cannot demote last owner.", true);
- await loadTeam(accountID);
+ await loadAccessRoster(accountID);
return;
}
deps.showToast(error instanceof Error ? error.message : "Failed to update role.", true);
- await loadTeam(accountID);
+ await loadAccessRoster(accountID);
}
};
var removeMember = async function(accountID, userID, email) {
if (!window.confirm("Remove " + email + " from this account?")) return;
try {
await deps.api.removeMember(accountID, userID);
- if (!await refreshAccountTeamSection(accountID)) {
+ if (!await refreshAccountAccessSection(accountID)) {
return;
}
deps.showToast("Member removed.");
@@ -1016,8 +1016,8 @@
selectWorkspace,
clearWorkspaceSelection,
openBilling,
- toggleTeam,
- ensureTeamVisible,
+ toggleAccess,
+ ensureAccessVisible,
inviteMember,
createWorkspace,
manageWorkspaceAction,
@@ -1129,7 +1129,7 @@
};
}
- // src/services_view.ts
+ // src/billing_view.ts
function getElement3(id) {
return document.getElementById(id);
}
@@ -1162,16 +1162,16 @@
el.value = value;
}
}
- function renderStatus(id, status) {
+ function renderBillingStatus(id, status) {
var el = getElement3(id);
if (!el) return;
if (!status.visible) {
el.textContent = "";
- el.className = "service-status";
+ el.className = "billing-status";
return;
}
el.textContent = status.message;
- el.className = "service-status visible" + (status.error ? " error" : " success");
+ el.className = "billing-status visible" + (status.error ? " error" : " success");
}
function renderButton(id, disabled, label) {
if (!id || !label) return;
@@ -1180,47 +1180,47 @@
button.disabled = disabled;
button.textContent = label;
}
- function renderOpenPanels(openPanelID) {
- var panels = ["manage-service-panel", "retrieve-service-panel", "refund-service-panel", "data-service-panel"];
- var emptyPanel = getElement3("service-panel-empty");
+ function renderOpenBillingPanels(openBillingPanelID) {
+ var panels = ["manage-billing-panel", "retrieve-billing-panel", "refund-billing-panel", "data-billing-panel"];
+ var emptyPanel = getElement3("billing-panel-empty");
if (emptyPanel) {
- emptyPanel.classList.toggle("visible", !openPanelID);
+ emptyPanel.classList.toggle("visible", !openBillingPanelID);
}
for (var i = 0; i < panels.length; i++) {
var panel = getElement3(panels[i]);
if (!panel) continue;
- panel.classList.toggle("visible", panels[i] === openPanelID);
+ panel.classList.toggle("visible", panels[i] === openBillingPanelID);
}
- var serviceButtons = document.querySelectorAll('[data-account-service-action="open-service-panel"]');
- for (var j = 0; j < serviceButtons.length; j += 1) {
- var button = serviceButtons[j];
- var row = button.closest(".service-action-row");
+ var billingButtons = document.querySelectorAll('[data-account-billing-action="open-billing-panel"]');
+ for (var j = 0; j < billingButtons.length; j += 1) {
+ var button = billingButtons[j];
+ var row = button.closest(".billing-action-row");
if (!row) continue;
- row.classList.toggle("active", button.getAttribute("data-account-service-panel") === openPanelID);
+ row.classList.toggle("active", button.getAttribute("data-account-billing-panel") === openBillingPanelID);
}
}
function renderRefundPanel(refundState, bootstrap) {
- var root = getElement3("refund-service-root");
+ var root = getElement3("refund-billing-root");
if (!root) return;
var refundSupportURL = (bootstrap.public_site_url || "") + "/refund.html?email=" + encodeURIComponent(refundState.emailValue || "");
- root.innerHTML = '
Refund requests
Process an eligible self-serve refund for a self-hosted purchase. This revokes the associated license immediately.
Warning: completing a refund immediately revokes the affected license. This should only be used when the refund window and commercial contract allow it.
If this purchase is not eligible for self-serve refund, use the public support path instead: open refund support page.
';
+ root.innerHTML = '
Refund requests
Process an eligible self-serve refund for a self-hosted purchase. This revokes the associated license immediately.
Warning: completing a refund immediately revokes the affected license. This should only be used when the refund window and commercial contract allow it.
If this purchase is not eligible for self-serve refund, use the public support path instead: open refund support page.
';
}
function renderManagePanel(flowState) {
- var root = getElement3("manage-service-root");
+ var root = getElement3("manage-billing-root");
if (!root) return;
- root.innerHTML = '
Manage subscriptions
Request a verification code for the commercial email, then open the Stripe customer portal for billing changes, invoices, and subscription actions.
';
}
function renderRetrievePanel(flowState) {
- var root = getElement3("retrieve-service-root");
+ var root = getElement3("retrieve-billing-root");
if (!root) return;
var result = flowState.result;
var invoiceURL = result && result.invoice_url ? result.invoice_url : "#";
- root.innerHTML = '
Retrieve licenses
Request a verification code for the commercial email, then reveal the current active self-hosted license without leaving Pulse Account.
";
} else if (context.loginState.success) {
var successMessage = context.loginState.successMessage || "If that email is registered, a magic link is on the way.";
- statusHTML = '
';
}
-function createTeamControlCell(className: string): HTMLDivElement {
+function createAccessControlCell(className: string): HTMLDivElement {
var cell = document.createElement('div');
- cell.className = 'team-control-cell ' + className;
+ cell.className = 'access-control-cell ' + className;
return cell;
}
-function renderTeamRoleControl(accountID: string, member: PortalTeamMember, isOwner: boolean): HTMLElement {
- var currentRole = normalizedTeamRole(member.role);
- var group = createTeamControlCell('team-control-cell-role');
+function renderAccessRoleControl(accountID: string, member: PortalAccessMember, isOwner: boolean): HTMLElement {
+ var currentRole = normalizedAccessRole(member.role);
+ var group = createAccessControlCell('access-control-cell-role');
if (currentRole === 'owner' && !isOwner) {
var locked = document.createElement('span');
- locked.className = 'team-role-badge';
+ locked.className = 'access-role-badge';
locked.textContent = roleLabel(currentRole);
group.appendChild(locked);
return group;
}
var sel = document.createElement('select');
- sel.className = 'team-role-select';
+ sel.className = 'access-role-select';
var roles = isOwner ? ['owner', 'admin', 'tech', 'read_only'] : ['admin', 'tech', 'read_only'];
for (var j = 0; j < roles.length; j += 1) {
var opt = document.createElement('option');
@@ -265,11 +265,11 @@ function renderTeamRoleControl(accountID: string, member: PortalTeamMember, isOw
return group;
}
-function renderTeamMemberAction(accountID: string, member: PortalTeamMember, isOwner: boolean): HTMLElement | null {
- if (normalizedTeamRole(member.role) === 'owner' && !isOwner) {
- var locked = createTeamControlCell('team-control-cell-access');
+function renderAccessMemberAction(accountID: string, member: PortalAccessMember, isOwner: boolean): HTMLElement | null {
+ if (normalizedAccessRole(member.role) === 'owner' && !isOwner) {
+ var locked = createAccessControlCell('access-control-cell-access');
var lockedText = document.createElement('span');
- lockedText.className = 'team-control-locked';
+ lockedText.className = 'access-control-locked';
lockedText.textContent = 'Locked';
locked.appendChild(lockedText);
return locked;
@@ -283,49 +283,49 @@ function renderTeamMemberAction(accountID: string, member: PortalTeamMember, isO
btn.setAttribute('data-account-id', accountID);
btn.setAttribute('data-user-id', member.user_id);
btn.setAttribute('data-member-email', member.email);
- var group = createTeamControlCell('team-control-cell-access');
+ var group = createAccessControlCell('access-control-cell-access');
group.appendChild(btn);
return group;
}
-function renderTeamMemberRow(accountID: string, member: PortalTeamMember, isOwner: boolean): HTMLElement {
+function renderAccessMemberRow(accountID: string, member: PortalAccessMember, isOwner: boolean): HTMLElement {
var row = document.createElement('div');
- row.className = 'team-member-row';
+ row.className = 'access-member-row';
var identity = document.createElement('div');
- identity.className = 'team-member-identity';
+ identity.className = 'access-member-identity';
var topline = document.createElement('div');
- topline.className = 'team-member-topline';
+ topline.className = 'access-member-topline';
var email = document.createElement('div');
- email.className = 'team-member-email';
+ email.className = 'access-member-email';
email.textContent = member.email;
topline.appendChild(email);
var roleBadge = document.createElement('span');
- roleBadge.className = 'team-inline-role-badge';
+ roleBadge.className = 'access-inline-role-badge';
roleBadge.textContent = roleLabel(member.role);
topline.appendChild(roleBadge);
identity.appendChild(topline);
var caption = document.createElement('div');
- caption.className = 'team-member-caption';
+ caption.className = 'access-member-caption';
caption.textContent = roleCapabilityCopy(member.role);
identity.appendChild(caption);
row.appendChild(identity);
- row.appendChild(renderTeamRoleControl(accountID, member, isOwner));
- row.appendChild(renderTeamMemberAction(accountID, member, isOwner) || createTeamControlCell('team-control-cell-access'));
+ row.appendChild(renderAccessRoleControl(accountID, member, isOwner));
+ row.appendChild(renderAccessMemberAction(accountID, member, isOwner) || createAccessControlCell('access-control-cell-access'));
return row;
}
-function ensureRosterHead(container: HTMLElement): void {
- var existing = container.querySelector('.team-roster-head');
+function ensureAccessRosterHead(container: HTMLElement): void {
+ var existing = container.querySelector('.access-roster-head');
if (existing) return;
var head = document.createElement('div');
- head.className = 'team-roster-head';
+ head.className = 'access-roster-head';
head.innerHTML =
'Operator' +
'Role' +
@@ -343,31 +343,31 @@ export function renderAddWorkspaceSection(accountID: string, entry: PortalAccoun
}
}
-export function renderTeamSection(accountID: string, entry: PortalAccountUIEntry): void {
- var section = getElement('team-section-' + accountID);
- var roster = getElement('team-list-' + accountID);
+export function renderAccessSection(accountID: string, entry: PortalAccountUIEntry): void {
+ var section = getElement('access-section-' + accountID);
+ var roster = getElement('access-list-' + accountID);
if (!section || !roster) return;
- var rosterPanel = roster.closest('.team-roster') as HTMLElement | null;
+ var rosterPanel = roster.closest('.access-roster') as HTMLElement | null;
var actorRole = section.getAttribute('data-actor-role') || '';
var isOwner = actorRole === 'owner';
- section.classList.toggle('visible', entry.teamVisible);
- renderTeamStats(accountID, entry);
+ section.classList.toggle('visible', entry.accessVisible);
+ renderAccessStats(accountID, entry);
- if (!entry.teamVisible) {
+ if (!entry.accessVisible) {
return;
}
- if (entry.teamQuery.status === 'loading') {
+ if (entry.accessQuery.status === 'loading') {
if (rosterPanel) rosterPanel.classList.add('state-only');
setContainerMessage(roster, 'Loading roster', 'Checking who currently has access to this account.', false);
return;
}
- if (entry.teamQuery.status === 'error') {
+ if (entry.accessQuery.status === 'error') {
if (rosterPanel) rosterPanel.classList.add('state-only');
- setContainerMessage(roster, 'Roster needs attention', entry.teamQuery.error, true);
+ setContainerMessage(roster, 'Roster needs attention', entry.accessQuery.error, true);
return;
}
- if (!entry.teamQuery.data.length) {
+ if (!entry.accessQuery.data.length) {
if (rosterPanel) rosterPanel.classList.add('state-only');
setContainerMessage(roster, 'No one added yet', 'Invite someone new when this account needs shared access.', false);
return;
@@ -376,10 +376,10 @@ export function renderTeamSection(accountID: string, entry: PortalAccountUIEntry
roster.textContent = '';
roster.classList.remove('state-only');
if (rosterPanel) rosterPanel.classList.remove('state-only');
- ensureRosterHead(roster);
- for (var i = 0; i < entry.teamQuery.data.length; i += 1) {
- var member = entry.teamQuery.data[i];
- roster.appendChild(renderTeamMemberRow(accountID, member, isOwner));
+ ensureAccessRosterHead(roster);
+ for (var i = 0; i < entry.accessQuery.data.length; i += 1) {
+ var member = entry.accessQuery.data[i];
+ roster.appendChild(renderAccessMemberRow(accountID, member, isOwner));
}
}
@@ -397,6 +397,6 @@ export function renderAccountUI(accountState: PortalAccountState, accounts: Port
}
renderAddWorkspaceSection(accountID, entry);
if (account) renderWorkspaceManagement(account, entry);
- renderTeamSection(accountID, entry);
+ renderAccessSection(accountID, entry);
}
}
diff --git a/internal/cloudcp/portal/frontend/src/api.ts b/internal/cloudcp/portal/frontend/src/api.ts
index ab1c1d8be..c4b62c1ab 100644
--- a/internal/cloudcp/portal/frontend/src/api.ts
+++ b/internal/cloudcp/portal/frontend/src/api.ts
@@ -1,4 +1,4 @@
-import type { PortalBootstrapData, PortalTeamMember } from './types';
+import type { PortalBootstrapData, PortalAccessMember } from './types';
interface PortalAPIContext {
getBootstrap(): PortalBootstrapData;
@@ -46,7 +46,7 @@ export interface PortalAPI {
suspendWorkspace(accountID: string, tenantID: string): Promise;
deleteWorkspace(accountID: string, tenantID: string): Promise;
openBilling(accountID: string): Promise;
- listMembers(accountID: string): Promise;
+ listMembers(accountID: string): Promise;
inviteMember(accountID: string, body: PortalMemberInviteRequest): Promise;
updateMemberRole(accountID: string, userID: string, body: PortalMemberRoleRequest): Promise;
removeMember(accountID: string, userID: string): Promise;
@@ -172,7 +172,7 @@ export function createPortalAPI(context: PortalAPIContext): PortalAPI {
}, 'Failed to open billing portal.');
},
listMembers: function(accountID: string) {
- return request(accountURL(accountID, '/members'), {}, 'Failed to load team.');
+ return request(accountURL(accountID, '/members'), {}, 'Failed to load access roster.');
},
inviteMember: function(accountID: string, body: PortalMemberInviteRequest) {
return request(accountURL(accountID, '/members'), {
diff --git a/internal/cloudcp/portal/frontend/src/app.test.ts b/internal/cloudcp/portal/frontend/src/app.test.ts
index a39fb07b5..e84998dbd 100644
--- a/internal/cloudcp/portal/frontend/src/app.test.ts
+++ b/internal/cloudcp/portal/frontend/src/app.test.ts
@@ -147,7 +147,7 @@ describe('portal app', function() {
},
{
email: 'buyer@example.com',
- openPanelID: 'retrieve-service-panel',
+ openBillingPanelID: 'retrieve-billing-panel',
}
);
@@ -175,9 +175,9 @@ describe('portal app', function() {
],
});
- expect(document.getElementById('retrieve-service-panel')?.classList.contains('visible')).toBe(true);
+ expect(document.getElementById('retrieve-billing-panel')?.classList.contains('visible')).toBe(true);
expect((document.getElementById('retrieve-inline-email') as HTMLInputElement | null)?.value).toBe('buyer@example.com');
- expect(runtime.store.getServiceState().openPanelID).toBe('retrieve-service-panel');
+ expect(runtime.store.getBillingState().openBillingPanelID).toBe('retrieve-billing-panel');
});
it('completes the signed-out magic-link flow through the real shell and auth runtime', async function() {
@@ -263,8 +263,8 @@ describe('portal app', function() {
});
await app.startupRefresh;
- document.getElementById('open-retrieve-service')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
- var retrievePanel = document.getElementById('retrieve-service-panel');
+ document.getElementById('open-retrieve-billing')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+ var retrievePanel = document.getElementById('retrieve-billing-panel');
expect(retrievePanel?.classList.contains('visible')).toBe(true);
var emailInput = document.getElementById('retrieve-inline-email') as HTMLInputElement | null;
@@ -294,7 +294,7 @@ describe('portal app', function() {
body: JSON.stringify({ email: 'owner@example.com', code: '123456' }),
}),
]);
- expect((store.getServiceState().flows.retrieve.result as { token?: string } | null)?.token).toBe('pulse_token_123');
+ expect((store.getBillingState().flows.retrieve.result as { token?: string } | null)?.token).toBe('pulse_token_123');
expect((document.getElementById('retrieve-inline-result') as HTMLElement | null)?.hidden).toBe(false);
expect((document.getElementById('retrieve-inline-copy') as HTMLButtonElement | null)?.hidden).toBe(false);
expect(document.getElementById('retrieve-inline-status')?.textContent).toContain('License retrieved successfully.');
@@ -339,7 +339,7 @@ describe('portal app', function() {
await flushAsync();
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/accounts/acct_1/members');
- expect(document.getElementById('team-section-acct_1')?.classList.contains('visible')).toBe(true);
- expect(document.getElementById('team-list-acct_1')?.textContent).toContain('owner@example.com');
+ expect(document.getElementById('access-section-acct_1')?.classList.contains('visible')).toBe(true);
+ expect(document.getElementById('access-list-acct_1')?.textContent).toContain('owner@example.com');
});
});
diff --git a/internal/cloudcp/portal/frontend/src/app.ts b/internal/cloudcp/portal/frontend/src/app.ts
index f1ec173ac..aa6403751 100644
--- a/internal/cloudcp/portal/frontend/src/app.ts
+++ b/internal/cloudcp/portal/frontend/src/app.ts
@@ -2,7 +2,7 @@ import { installAccountController } from './account_controller';
import { installAccountRuntime } from './account_runtime';
import { createPortalAPI, PortalAPIError } from './api';
import { installAuthController } from './auth_controller';
-import { installServicesRuntime } from './services';
+import { installBillingRuntime } from './billing';
import { installShell } from './shell';
import { createAnonymousBootstrap } from './store';
import { createPortalRuntime } from './runtime';
@@ -74,13 +74,13 @@ export function installPortalApp(deps: PortalAppDeps): PortalApp {
if (section === 'access') {
var accounts = deps.store.getBootstrap().accounts || [];
for (var i = 0; i < accounts.length; i += 1) {
- accountRuntime.ensureTeamVisible(accounts[i].id);
+ accountRuntime.ensureAccessVisible(accounts[i].id);
}
}
},
});
- installServicesRuntime({
+ installBillingRuntime({
api: api,
store: deps.store,
});
diff --git a/internal/cloudcp/portal/frontend/src/services.ts b/internal/cloudcp/portal/frontend/src/billing.ts
similarity index 61%
rename from internal/cloudcp/portal/frontend/src/services.ts
rename to internal/cloudcp/portal/frontend/src/billing.ts
index a843757b2..f18f5cf80 100644
--- a/internal/cloudcp/portal/frontend/src/services.ts
+++ b/internal/cloudcp/portal/frontend/src/billing.ts
@@ -1,16 +1,16 @@
import { beginMutationState, failMutationState, succeedMutationState } from './async_state';
import type { PortalAPI } from './api';
-import { installServicesController } from './services_controller';
+import { installBillingController } from './billing_controller';
import {
clearFlowStatus,
- createPortalServiceState,
+ createPortalBillingState,
emptyStatus,
resetVerificationFlowState,
setFlowStatus,
setRefundStatus,
- toggleServicePanelState,
+ toggleBillingPanelState,
updateDeleteConfirmation,
- updateServiceInputValue,
+ updateBillingInputValue,
} from './state';
import type { PortalStore } from './store';
import {
@@ -22,14 +22,14 @@ import {
renderExportPanel,
renderExportResult,
renderManagePanel,
- renderOpenPanels,
+ renderOpenBillingPanels,
renderRefundPanel,
renderRetrievePanel,
- renderStatus,
+ renderBillingStatus,
setValue,
setVisible,
-} from './services_view';
-import type { PortalServiceFlowID, PortalServiceState, VerificationFlowState } from './types';
+} from './billing_view';
+import type { PortalBillingFlowID, PortalBillingState, VerificationFlowState } from './types';
interface VerificationFlowDefinition {
requestPath: string;
@@ -53,54 +53,54 @@ interface VerificationFlowDefinition {
readCodeValue?: () => string;
onRequestStart?: () => void;
beforeConfirm?: () => boolean;
- applyConfirmSuccessState?: (serviceState: PortalServiceState, data: any, email?: string) => void;
+ applyConfirmSuccessState?: (billingState: PortalBillingState, data: any, email?: string) => void;
afterConfirmSuccess?: (data: any, email?: string) => void;
renderPanel: (flowState: VerificationFlowState) => void;
renderResult?: (result: unknown) => void;
}
-export interface ServicesRuntimeDeps {
+export interface BillingRuntimeDeps {
api: PortalAPI;
store: PortalStore;
}
-export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
+export function installBillingRuntime(deps: BillingRuntimeDeps): void {
var api = deps.api;
var store = deps.store;
- store.updateServiceState(function(serviceState) {
- if (!serviceState.flows) {
- var nextState = createPortalServiceState();
- serviceState.openPanelID = nextState.openPanelID;
- serviceState.flows = nextState.flows;
- serviceState.refund = nextState.refund;
+ store.updateBillingState(function(billingState) {
+ if (!billingState.flows) {
+ var nextState = createPortalBillingState();
+ billingState.openBillingPanelID = nextState.openBillingPanelID;
+ billingState.flows = nextState.flows;
+ billingState.refund = nextState.refund;
}
}, { notify: false });
- function getServiceState() {
- return store.getServiceState();
+ function getBillingState() {
+ return store.getBillingState();
}
- function updateServiceState(mutator, notify = true) {
- return store.updateServiceState(mutator, { notify: notify });
+ function updateBillingState(mutator, notify = true) {
+ return store.updateBillingState(mutator, { notify: notify });
}
- function toggleServicePanel(panelID) {
- updateServiceState(function(serviceState) {
- toggleServicePanelState(serviceState, panelID);
+ function toggleBillingPanel(panelID) {
+ updateBillingState(function(billingState) {
+ toggleBillingPanelState(billingState, panelID);
});
}
- function renderFlow(flowID: PortalServiceFlowID) {
+ function renderFlow(flowID: PortalBillingFlowID) {
var flow = verificationFlows[flowID];
if (!flow) return;
- var flowState = getServiceState().flows[flowID];
+ var flowState = getBillingState().flows[flowID];
if (flow.renderPanel) {
flow.renderPanel(flowState);
}
renderButton(flow.requestButtonID, flowState.request.pending, flowState.request.pending ? flow.requestPendingLabel : flow.requestLabel);
renderButton(flow.confirmButtonID, flowState.confirm.pending, flowState.confirm.pending ? flow.confirmPendingLabel : flow.confirmLabel);
- renderStatus(flow.statusID, flowState.status);
+ renderBillingStatus(flow.statusID, flowState.status);
if (flow.step2ID) {
setVisible(flow.step2ID, flowState.step2Visible);
}
@@ -118,28 +118,28 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
}
function renderRefund() {
- var refundState = getServiceState().refund;
+ var refundState = getBillingState().refund;
renderRefundPanel(refundState, store.getBootstrap());
renderButton('refund-inline-submit', refundState.submit.pending, refundState.submit.pending ? 'Processing...' : 'Process Refund');
- renderStatus('refund-inline-status', refundState.status);
+ renderBillingStatus('refund-inline-status', refundState.status);
}
- function resetVerificationFlow(flowID: PortalServiceFlowID) {
+ function resetVerificationFlow(flowID: PortalBillingFlowID) {
var flow = verificationFlows[flowID];
if (!flow) return;
- updateServiceState(function(serviceState) {
- resetVerificationFlowState(serviceState, flowID);
+ updateBillingState(function(billingState) {
+ resetVerificationFlowState(billingState, flowID);
}, false);
if (flow.codeInputID) {
setValue(flow.codeInputID, '');
}
}
- var verificationFlows: Record = {
+ var verificationFlows: Record = {
manage: {
requestPath: '/v1/manage/request',
confirmPath: '/v1/manage',
- panelID: 'manage-service-panel',
+ panelID: 'manage-billing-panel',
emailInputID: 'manage-inline-email',
codeInputID: 'manage-inline-code',
requestButtonID: 'manage-inline-request',
@@ -155,10 +155,10 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
requestErrorMessage: 'Failed to send verification code',
confirmErrorMessage: 'Failed to open customer portal',
readEmailValue: function() {
- return getServiceState().flows.manage.emailValue;
+ return getBillingState().flows.manage.emailValue;
},
readCodeValue: function() {
- return getServiceState().flows.manage.codeValue;
+ return getBillingState().flows.manage.codeValue;
},
onRequestStart: function() {},
afterConfirmSuccess: function(data) {
@@ -169,7 +169,7 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
retrieve: {
requestPath: '/v1/retrieve-license/request',
confirmPath: '/v1/retrieve-license',
- panelID: 'retrieve-service-panel',
+ panelID: 'retrieve-billing-panel',
emailInputID: 'retrieve-inline-email',
codeInputID: 'retrieve-inline-code',
requestButtonID: 'retrieve-inline-request',
@@ -185,27 +185,27 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
requestErrorMessage: 'Failed to send verification code',
confirmErrorMessage: 'Failed to retrieve license',
readEmailValue: function() {
- return getServiceState().flows.retrieve.emailValue;
+ return getBillingState().flows.retrieve.emailValue;
},
readCodeValue: function() {
- return getServiceState().flows.retrieve.codeValue;
+ return getBillingState().flows.retrieve.codeValue;
},
onRequestStart: function() {
- updateServiceState(function(serviceState) {
- serviceState.flows.retrieve.result = null;
+ updateBillingState(function(billingState) {
+ billingState.flows.retrieve.result = null;
}, false);
},
- applyConfirmSuccessState: function(serviceState, data) {
- serviceState.flows.retrieve.result = data.license;
- serviceState.flows.retrieve.codeValue = '';
- setFlowStatus(serviceState, 'retrieve', 'License retrieved successfully.', false);
+ applyConfirmSuccessState: function(billingState, data) {
+ billingState.flows.retrieve.result = data.license;
+ billingState.flows.retrieve.codeValue = '';
+ setFlowStatus(billingState, 'retrieve', 'License retrieved successfully.', false);
},
renderPanel: renderRetrievePanel,
},
export: {
requestPath: '/v1/gdpr/request-export',
confirmPath: '/v1/gdpr/export',
- panelID: 'data-service-panel',
+ panelID: 'data-billing-panel',
emailInputID: 'data-export-email',
codeInputID: 'data-export-code',
requestButtonID: 'data-export-request',
@@ -221,22 +221,22 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
requestErrorMessage: 'Request failed',
confirmErrorMessage: 'Export failed',
readEmailValue: function() {
- return getServiceState().flows.export.emailValue;
+ return getBillingState().flows.export.emailValue;
},
readCodeValue: function() {
- return getServiceState().flows.export.codeValue;
+ return getBillingState().flows.export.codeValue;
},
onRequestStart: function() {
- updateServiceState(function(serviceState) {
- serviceState.flows.export.result = null;
+ updateBillingState(function(billingState) {
+ billingState.flows.export.result = null;
}, false);
},
- applyConfirmSuccessState: function(serviceState, data) {
- var emailValue = serviceState.flows.export.emailValue;
- resetVerificationFlowState(serviceState, 'export');
- serviceState.flows.export.emailValue = emailValue;
- serviceState.flows.export.result = data;
- setFlowStatus(serviceState, 'export', 'Data export retrieved successfully.', false);
+ applyConfirmSuccessState: function(billingState, data) {
+ var emailValue = billingState.flows.export.emailValue;
+ resetVerificationFlowState(billingState, 'export');
+ billingState.flows.export.emailValue = emailValue;
+ billingState.flows.export.result = data;
+ setFlowStatus(billingState, 'export', 'Data export retrieved successfully.', false);
},
renderPanel: renderExportPanel,
renderResult: renderExportResult,
@@ -244,7 +244,7 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
delete: {
requestPath: '/v1/gdpr/request-delete',
confirmPath: '/v1/gdpr/confirm-delete',
- panelID: 'data-service-panel',
+ panelID: 'data-billing-panel',
emailInputID: 'data-delete-email',
codeInputID: 'data-delete-code',
requestButtonID: 'data-delete-request',
@@ -260,26 +260,26 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
requestErrorMessage: 'Request failed',
confirmErrorMessage: 'Deletion failed',
readEmailValue: function() {
- return getServiceState().flows.delete.emailValue;
+ return getBillingState().flows.delete.emailValue;
},
readCodeValue: function() {
- return getServiceState().flows.delete.codeValue;
+ return getBillingState().flows.delete.codeValue;
},
beforeConfirm: function() {
if (!getElement('data-delete-confirm-check')?.checked) {
- updateServiceState(function(serviceState) {
- setFlowStatus(serviceState, 'delete', 'You must confirm that you understand this action is permanent.', true);
+ updateBillingState(function(billingState) {
+ setFlowStatus(billingState, 'delete', 'You must confirm that you understand this action is permanent.', true);
});
return false;
}
return true;
},
- applyConfirmSuccessState: function(serviceState, data) {
- var emailValue = serviceState.flows.delete.emailValue;
- resetVerificationFlowState(serviceState, 'delete');
- serviceState.flows.delete.emailValue = emailValue;
+ applyConfirmSuccessState: function(billingState, data) {
+ var emailValue = billingState.flows.delete.emailValue;
+ resetVerificationFlowState(billingState, 'delete');
+ billingState.flows.delete.emailValue = emailValue;
setFlowStatus(
- serviceState,
+ billingState,
'delete',
data.deleted_count > 0 && data.stripe_reminder ? data.message + ' ' + data.stripe_reminder : data.message,
false
@@ -295,7 +295,7 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
},
};
- async function requestVerificationCode(flowID: PortalServiceFlowID) {
+ async function requestVerificationCode(flowID: PortalBillingFlowID) {
var flow = verificationFlows[flowID];
if (!flow) return;
var email = flow.readEmailValue ? flow.readEmailValue() : readValue(flow.emailInputID);
@@ -306,63 +306,63 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
if (flow.onRequestStart) {
flow.onRequestStart();
}
- updateServiceState(function(serviceState) {
- beginMutationState(serviceState.flows[flowID].request);
- clearFlowStatus(serviceState, flowID);
+ updateBillingState(function(billingState) {
+ beginMutationState(billingState.flows[flowID].request);
+ clearFlowStatus(billingState, flowID);
});
try {
await api.postCommercialJSON(flow.requestPath, { email: email });
- updateServiceState(function(serviceState) {
- serviceState.flows[flowID].pendingEmail = email;
- serviceState.flows[flowID].step2Visible = !!flow.step2ID;
- succeedMutationState(serviceState.flows[flowID].request);
- setFlowStatus(serviceState, flowID, flow.requestSuccessMessage, false);
+ updateBillingState(function(billingState) {
+ billingState.flows[flowID].pendingEmail = email;
+ billingState.flows[flowID].step2Visible = !!flow.step2ID;
+ succeedMutationState(billingState.flows[flowID].request);
+ setFlowStatus(billingState, flowID, flow.requestSuccessMessage, false);
});
} catch (err) {
var message = err instanceof Error ? err.message : flow.requestErrorMessage;
- updateServiceState(function(serviceState) {
- failMutationState(serviceState.flows[flowID].request, message);
- setFlowStatus(serviceState, flowID, message, true);
+ updateBillingState(function(billingState) {
+ failMutationState(billingState.flows[flowID].request, message);
+ setFlowStatus(billingState, flowID, message, true);
});
}
}
- async function resendVerificationCode(flowID: PortalServiceFlowID, event?: Event) {
+ async function resendVerificationCode(flowID: PortalBillingFlowID, event?: Event) {
if (event) event.preventDefault();
var flow = verificationFlows[flowID];
if (!flow) return;
- var email = getServiceState().flows[flowID].pendingEmail;
+ var email = getBillingState().flows[flowID].pendingEmail;
if (!email) return;
try {
await api.postCommercialJSON(flow.requestPath, { email: email });
- updateServiceState(function(serviceState) {
- setFlowStatus(serviceState, flowID, flow.resendSuccessMessage, false);
+ updateBillingState(function(billingState) {
+ setFlowStatus(billingState, flowID, flow.resendSuccessMessage, false);
});
} catch (err) {
- updateServiceState(function(serviceState) {
- setFlowStatus(serviceState, flowID, err instanceof Error ? err.message : flow.requestErrorMessage, true);
+ updateBillingState(function(billingState) {
+ setFlowStatus(billingState, flowID, err instanceof Error ? err.message : flow.requestErrorMessage, true);
});
}
}
- async function confirmVerificationCode(flowID: PortalServiceFlowID) {
+ async function confirmVerificationCode(flowID: PortalBillingFlowID) {
var flow = verificationFlows[flowID];
if (!flow) return;
- var email = getServiceState().flows[flowID].pendingEmail;
+ var email = getBillingState().flows[flowID].pendingEmail;
var code = flow.readCodeValue ? flow.readCodeValue() : readValue(flow.codeInputID);
if (!email || !code) return;
if (flow.beforeConfirm && flow.beforeConfirm() === false) {
return;
}
- updateServiceState(function(serviceState) {
- beginMutationState(serviceState.flows[flowID].confirm);
+ updateBillingState(function(billingState) {
+ beginMutationState(billingState.flows[flowID].confirm);
});
try {
var data = await api.postCommercialJSON(flow.confirmPath, { email: email, code: code });
- updateServiceState(function(serviceState) {
- succeedMutationState(serviceState.flows[flowID].confirm);
+ updateBillingState(function(billingState) {
+ succeedMutationState(billingState.flows[flowID].confirm);
if (flow.applyConfirmSuccessState) {
- flow.applyConfirmSuccessState(serviceState, data, email);
+ flow.applyConfirmSuccessState(billingState, data, email);
}
});
if (flow.afterConfirmSuccess) {
@@ -370,68 +370,68 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
}
} catch (err) {
var message = err instanceof Error ? err.message : flow.confirmErrorMessage;
- updateServiceState(function(serviceState) {
- failMutationState(serviceState.flows[flowID].confirm, message);
- setFlowStatus(serviceState, flowID, message, true);
+ updateBillingState(function(billingState) {
+ failMutationState(billingState.flows[flowID].confirm, message);
+ setFlowStatus(billingState, flowID, message, true);
});
}
}
async function copyRetrievedLicense() {
- var result = getServiceState().flows.retrieve.result as { token?: string } | null;
+ var result = getBillingState().flows.retrieve.result as { token?: string } | null;
var token = result && result.token ? result.token : '';
if (!token) return;
try {
await navigator.clipboard.writeText(token);
- updateServiceState(function(serviceState) {
- setFlowStatus(serviceState, 'retrieve', 'License key copied to clipboard.', false);
+ updateBillingState(function(billingState) {
+ setFlowStatus(billingState, 'retrieve', 'License key copied to clipboard.', false);
});
} catch (_) {
- updateServiceState(function(serviceState) {
- setFlowStatus(serviceState, 'retrieve', 'Failed to copy automatically. Please copy the key manually.', true);
+ updateBillingState(function(billingState) {
+ setFlowStatus(billingState, 'retrieve', 'Failed to copy automatically. Please copy the key manually.', true);
});
}
}
async function submitRefund() {
- var email = getServiceState().refund.emailValue;
- var token = getServiceState().refund.tokenValue;
+ var email = getBillingState().refund.emailValue;
+ var token = getBillingState().refund.tokenValue;
if (!email || !token) return;
if (!confirm('Are you sure? This will immediately revoke the license and request the refund.')) return;
- updateServiceState(function(serviceState) {
- beginMutationState(serviceState.refund.submit);
- serviceState.refund.status = emptyStatus();
+ updateBillingState(function(billingState) {
+ beginMutationState(billingState.refund.submit);
+ billingState.refund.status = emptyStatus();
});
try {
await api.postCommercialJSON('/v1/self-refund', { email: email, token: token });
- updateServiceState(function(serviceState) {
- serviceState.refund.tokenValue = '';
- succeedMutationState(serviceState.refund.submit);
- setRefundStatus(serviceState, 'Success! Your refund has been processed. Stripe will follow up by email.', false);
+ updateBillingState(function(billingState) {
+ billingState.refund.tokenValue = '';
+ succeedMutationState(billingState.refund.submit);
+ setRefundStatus(billingState, 'Success! Your refund has been processed. Stripe will follow up by email.', false);
});
} catch (err) {
var message = err instanceof Error ? err.message : 'Refund failed';
- updateServiceState(function(serviceState) {
- failMutationState(serviceState.refund.submit, message);
- setRefundStatus(serviceState, message, true);
+ updateBillingState(function(billingState) {
+ failMutationState(billingState.refund.submit, message);
+ setRefundStatus(billingState, message, true);
});
}
}
- function renderServiceRuntime() {
- renderOpenPanels(getServiceState().openPanelID);
+ function renderBillingRuntime() {
+ renderOpenBillingPanels(getBillingState().openBillingPanelID);
renderAllFlows();
}
- renderServiceRuntime();
- store.subscribeBootstrap(renderServiceRuntime);
- store.subscribeServices(renderServiceRuntime);
+ renderBillingRuntime();
+ store.subscribeBootstrap(renderBillingRuntime);
+ store.subscribeBilling(renderBillingRuntime);
- installServicesController({
+ installBillingController({
setShellSection: function(section) {
store.setActiveShellSection(section);
},
- toggleServicePanel,
+ toggleBillingPanel,
focusElement,
requestVerificationCode: function(flowID) {
void requestVerificationCode(flowID);
@@ -449,13 +449,13 @@ export function installServicesRuntime(deps: ServicesRuntimeDeps): void {
void submitRefund();
},
updateInputValue: function(inputKind, value) {
- updateServiceState(function(serviceState) {
- updateServiceInputValue(serviceState, inputKind, value);
+ updateBillingState(function(billingState) {
+ updateBillingInputValue(billingState, inputKind, value);
}, false);
},
updateDeleteConfirmation: function(checked) {
- updateServiceState(function(serviceState) {
- updateDeleteConfirmation(serviceState, checked);
+ updateBillingState(function(billingState) {
+ updateDeleteConfirmation(billingState, checked);
}, false);
},
});
diff --git a/internal/cloudcp/portal/frontend/src/services_controller.test.ts b/internal/cloudcp/portal/frontend/src/billing_controller.test.ts
similarity index 77%
rename from internal/cloudcp/portal/frontend/src/services_controller.test.ts
rename to internal/cloudcp/portal/frontend/src/billing_controller.test.ts
index e37ea52af..102df989c 100644
--- a/internal/cloudcp/portal/frontend/src/services_controller.test.ts
+++ b/internal/cloudcp/portal/frontend/src/billing_controller.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { installServicesController } from './services_controller';
+import { installBillingController } from './billing_controller';
describe('services controller', function() {
beforeEach(function() {
@@ -11,7 +11,7 @@ describe('services controller', function() {
it('routes service actions to the matching handlers', function() {
var deps = {
setShellSection: vi.fn(),
- toggleServicePanel: vi.fn(),
+ toggleBillingPanel: vi.fn(),
focusElement: vi.fn(),
requestVerificationCode: vi.fn(),
resendVerificationCode: vi.fn(),
@@ -22,19 +22,19 @@ describe('services controller', function() {
updateDeleteConfirmation: vi.fn(),
};
- installServicesController(deps);
+ installBillingController(deps);
document.body.innerHTML =
- '' +
- '' +
- '' +
- '' +
- '' +
- '';
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '';
document.getElementById('open')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
expect(deps.setShellSection).toHaveBeenCalledWith('billing');
- expect(deps.toggleServicePanel).toHaveBeenCalledWith('retrieve-service-panel');
+ expect(deps.toggleBillingPanel).toHaveBeenCalledWith('retrieve-billing-panel');
expect(deps.focusElement).toHaveBeenCalledWith('retrieve-inline-email');
document.getElementById('request')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
@@ -56,7 +56,7 @@ describe('services controller', function() {
it('routes input and checkbox changes into state update hooks', function() {
var deps = {
setShellSection: vi.fn(),
- toggleServicePanel: vi.fn(),
+ toggleBillingPanel: vi.fn(),
focusElement: vi.fn(),
requestVerificationCode: vi.fn(),
resendVerificationCode: vi.fn(),
@@ -67,10 +67,10 @@ describe('services controller', function() {
updateDeleteConfirmation: vi.fn(),
};
- installServicesController(deps);
+ installBillingController(deps);
document.body.innerHTML =
- '' +
+ '' +
'';
var emailInput = document.getElementById('retrieve-email') as HTMLInputElement;
diff --git a/internal/cloudcp/portal/frontend/src/services_controller.ts b/internal/cloudcp/portal/frontend/src/billing_controller.ts
similarity index 84%
rename from internal/cloudcp/portal/frontend/src/services_controller.ts
rename to internal/cloudcp/portal/frontend/src/billing_controller.ts
index 58a46de56..60102f8da 100644
--- a/internal/cloudcp/portal/frontend/src/services_controller.ts
+++ b/internal/cloudcp/portal/frontend/src/billing_controller.ts
@@ -1,8 +1,8 @@
-import { asHTMLElement } from './services_view';
+import { asHTMLElement } from './billing_view';
-export interface ServicesControllerDeps {
+export interface BillingControllerDeps {
setShellSection: (section: 'overview' | 'workspaces' | 'access' | 'billing' | 'support') => void;
- toggleServicePanel: (panelID: string) => void;
+ toggleBillingPanel: (panelID: string) => void;
focusElement: (id: string) => void;
requestVerificationCode: (flowID: 'manage' | 'retrieve' | 'export' | 'delete') => void;
resendVerificationCode: (flowID: 'manage' | 'export' | 'delete', event?: Event) => void;
@@ -13,19 +13,19 @@ export interface ServicesControllerDeps {
updateDeleteConfirmation: (checked: boolean) => void;
}
-export function installServicesController(deps: ServicesControllerDeps): void {
+export function installBillingController(deps: BillingControllerDeps): void {
document.addEventListener('click', function(event) {
- var target = asHTMLElement(event.target)?.closest('[data-account-service-action]');
+ var target = asHTMLElement(event.target)?.closest('[data-account-billing-action]');
if (!target) return;
- var action = target.getAttribute('data-account-service-action') || '';
- var panelID = target.getAttribute('data-account-service-panel') || '';
- var focusID = target.getAttribute('data-account-service-focus') || '';
+ var action = target.getAttribute('data-account-billing-action') || '';
+ var panelID = target.getAttribute('data-account-billing-panel') || '';
+ var focusID = target.getAttribute('data-account-billing-focus') || '';
switch (action) {
- case 'open-service-panel':
+ case 'open-billing-panel':
event.preventDefault();
deps.setShellSection('billing');
- deps.toggleServicePanel(panelID);
+ deps.toggleBillingPanel(panelID);
deps.focusElement(focusID);
return;
case 'manage-inline-request':
@@ -85,7 +85,7 @@ export function installServicesController(deps: ServicesControllerDeps): void {
document.addEventListener('input', function(event) {
var target = asHTMLElement(event.target) as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null;
if (!target) return;
- var inputKind = target.getAttribute('data-account-service-input') || '';
+ var inputKind = target.getAttribute('data-account-billing-input') || '';
if (!inputKind) return;
deps.updateInputValue(inputKind, target.value);
});
diff --git a/internal/cloudcp/portal/frontend/src/services_view.test.ts b/internal/cloudcp/portal/frontend/src/billing_view.test.ts
similarity index 80%
rename from internal/cloudcp/portal/frontend/src/services_view.test.ts
rename to internal/cloudcp/portal/frontend/src/billing_view.test.ts
index b7737789c..3617009c9 100644
--- a/internal/cloudcp/portal/frontend/src/services_view.test.ts
+++ b/internal/cloudcp/portal/frontend/src/billing_view.test.ts
@@ -5,12 +5,12 @@ import {
renderExportPanel,
renderExportResult,
renderManagePanel,
- renderOpenPanels,
+ renderOpenBillingPanels,
renderRefundPanel,
renderRetrievePanel,
- renderStatus,
-} from './services_view';
-import type { PortalBootstrapData, RefundState, ServiceStatus, VerificationFlowState } from './types';
+ renderBillingStatus,
+} from './billing_view';
+import type { PortalBootstrapData, RefundState, BillingStatus, VerificationFlowState } from './types';
function createBootstrap(overrides: Partial = {}): PortalBootstrapData {
return {
@@ -80,25 +80,25 @@ describe('services view', function() {
it('toggles the visible service panel by id', function() {
document.body.innerHTML =
- '' +
- '' +
- '' +
- '' +
- '' +
- '' +
- '';
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '';
- renderOpenPanels('retrieve-service-panel');
+ renderOpenBillingPanels('retrieve-billing-panel');
- expect(document.getElementById('retrieve-service-panel')?.classList.contains('visible')).toBe(true);
- expect(document.getElementById('manage-service-panel')?.classList.contains('visible')).toBe(false);
- expect(document.getElementById('refund-service-panel')?.classList.contains('visible')).toBe(false);
- expect(document.getElementById('service-panel-empty')?.classList.contains('visible')).toBe(false);
- expect(document.querySelectorAll('.service-action-row.active')).toHaveLength(1);
+ expect(document.getElementById('retrieve-billing-panel')?.classList.contains('visible')).toBe(true);
+ expect(document.getElementById('manage-billing-panel')?.classList.contains('visible')).toBe(false);
+ expect(document.getElementById('refund-billing-panel')?.classList.contains('visible')).toBe(false);
+ expect(document.getElementById('billing-panel-empty')?.classList.contains('visible')).toBe(false);
+ expect(document.querySelectorAll('.billing-action-row.active')).toHaveLength(1);
});
it('renders retrieve panel result state with invoice and token metadata', function() {
- document.body.innerHTML = '';
+ document.body.innerHTML = '';
renderRetrievePanel(
createFlowState({
@@ -130,7 +130,7 @@ describe('services view', function() {
it('renders refund and delete panels from owned state', function() {
document.body.innerHTML =
- '' +
+ '' +
'';
renderRefundPanel(
@@ -155,7 +155,7 @@ describe('services view', function() {
expect(refundEmail.value).toBe('owner@example.com');
expect(refundToken.value).toBe('pulse_token');
- expect(document.getElementById('refund-service-root')?.innerHTML).toContain('/refund.html?email=owner%40example.com');
+ expect(document.getElementById('refund-billing-root')?.innerHTML).toContain('/refund.html?email=owner%40example.com');
expect(deleteCheck.checked).toBe(true);
expect((document.getElementById('data-delete-step2') as HTMLElement).hidden).toBe(false);
});
@@ -189,8 +189,8 @@ describe('services view', function() {
it('renders manage panel and status classes from service state', function() {
document.body.innerHTML =
- '' +
- '';
+ '' +
+ '';
renderManagePanel(
createFlowState({
@@ -199,11 +199,11 @@ describe('services view', function() {
step2Visible: true,
})
);
- renderStatus('manage-inline-status', {
+ renderBillingStatus('manage-inline-status', {
visible: true,
message: 'Code sent.',
error: false,
- } satisfies ServiceStatus);
+ } satisfies BillingStatus);
expect((document.getElementById('manage-inline-email') as HTMLInputElement).value).toBe('owner@example.com');
expect((document.getElementById('manage-inline-code') as HTMLInputElement).value).toBe('123456');
diff --git a/internal/cloudcp/portal/frontend/src/services_view.ts b/internal/cloudcp/portal/frontend/src/billing_view.ts
similarity index 79%
rename from internal/cloudcp/portal/frontend/src/services_view.ts
rename to internal/cloudcp/portal/frontend/src/billing_view.ts
index a8fab11b4..1b30be7c8 100644
--- a/internal/cloudcp/portal/frontend/src/services_view.ts
+++ b/internal/cloudcp/portal/frontend/src/billing_view.ts
@@ -1,4 +1,4 @@
-import type { PortalBootstrapData, RefundState, ServiceStatus, VerificationFlowState } from './types';
+import type { PortalBootstrapData, RefundState, BillingStatus, VerificationFlowState } from './types';
type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
@@ -47,16 +47,16 @@ export function setValue(id: string, value: string): void {
}
}
-export function renderStatus(id: string, status: ServiceStatus): void {
+export function renderBillingStatus(id: string, status: BillingStatus): void {
var el = getElement(id);
if (!el) return;
if (!status.visible) {
el.textContent = '';
- el.className = 'service-status';
+ el.className = 'billing-status';
return;
}
el.textContent = status.message;
- el.className = 'service-status visible' + (status.error ? ' error' : ' success');
+ el.className = 'billing-status visible' + (status.error ? ' error' : ' success');
}
export function renderButton(id: string | undefined, disabled: boolean, label: string | undefined): void {
@@ -67,28 +67,28 @@ export function renderButton(id: string | undefined, disabled: boolean, label: s
button.textContent = label;
}
-export function renderOpenPanels(openPanelID: string): void {
- var panels = ['manage-service-panel', 'retrieve-service-panel', 'refund-service-panel', 'data-service-panel'];
- var emptyPanel = getElement('service-panel-empty');
+export function renderOpenBillingPanels(openBillingPanelID: string): void {
+ var panels = ['manage-billing-panel', 'retrieve-billing-panel', 'refund-billing-panel', 'data-billing-panel'];
+ var emptyPanel = getElement('billing-panel-empty');
if (emptyPanel) {
- emptyPanel.classList.toggle('visible', !openPanelID);
+ emptyPanel.classList.toggle('visible', !openBillingPanelID);
}
for (var i = 0; i < panels.length; i++) {
var panel = getElement(panels[i]);
if (!panel) continue;
- panel.classList.toggle('visible', panels[i] === openPanelID);
+ panel.classList.toggle('visible', panels[i] === openBillingPanelID);
}
- var serviceButtons = document.querySelectorAll('[data-account-service-action="open-service-panel"]');
- for (var j = 0; j < serviceButtons.length; j += 1) {
- var button = serviceButtons[j];
- var row = button.closest('.service-action-row');
+ var billingButtons = document.querySelectorAll('[data-account-billing-action="open-billing-panel"]');
+ for (var j = 0; j < billingButtons.length; j += 1) {
+ var button = billingButtons[j];
+ var row = button.closest('.billing-action-row');
if (!row) continue;
- row.classList.toggle('active', button.getAttribute('data-account-service-panel') === openPanelID);
+ row.classList.toggle('active', button.getAttribute('data-account-billing-panel') === openBillingPanelID);
}
}
export function renderRefundPanel(refundState: RefundState, bootstrap: PortalBootstrapData): void {
- var root = getElement('refund-service-root');
+ var root = getElement('refund-billing-root');
if (!root) return;
var refundSupportURL = (bootstrap.public_site_url || '') + '/refund.html?email=' + encodeURIComponent(refundState.emailValue || '');
root.innerHTML = '' +
@@ -97,21 +97,21 @@ export function renderRefundPanel(refundState: RefundState, bootstrap: PortalBoo
'
Warning: completing a refund immediately revokes the affected license. This should only be used when the refund window and commercial contract allow it.
' +
'
' +
'' +
- '' +
+ '' +
'
' +
'
' +
'' +
- '' +
+ '' +
'
' +
'
' +
- '' +
+ '' +
'
' +
'
If this purchase is not eligible for self-serve refund, use the public support path instead: open refund support page.
' +
- '';
+ '';
}
export function renderManagePanel(flowState: VerificationFlowState): void {
- var root = getElement('manage-service-root');
+ var root = getElement('manage-billing-root');
if (!root) return;
root.innerHTML = '' +
'