mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-08 18:21:55 +00:00
refactor(cloudcp): split Pulse Account service runtime
This commit is contained in:
parent
9fe66fa5c1
commit
8f65a3dca6
6 changed files with 628 additions and 527 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"source_hash": "276c1c649eee16c120f9bfe29e54582f9cc50c5a9c1c1b0acc99023b4dd8ac0f",
|
||||
"source_hash": "33ad099dc74b695667a57e6d0ee611f46bb9f70d4b875e5eb84d192ea0173742",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
"src/shell.ts",
|
||||
"src/shell_view.ts",
|
||||
"src/services.ts",
|
||||
"src/services_controller.ts",
|
||||
"src/services_view.ts",
|
||||
"src/runtime.ts",
|
||||
"src/types.ts",
|
||||
"src/styles.css"
|
||||
|
|
|
|||
403
internal/cloudcp/portal/dist/portal_app.js
vendored
403
internal/cloudcp/portal/dist/portal_app.js
vendored
|
|
@ -713,6 +713,183 @@
|
|||
refreshBootstrap();
|
||||
}
|
||||
|
||||
// src/services_view.ts
|
||||
function getElement3(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
function asHTMLElement3(target) {
|
||||
return target instanceof HTMLElement ? target : null;
|
||||
}
|
||||
function escapeText(value) {
|
||||
return String(value || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
function escapeAttribute(value) {
|
||||
return escapeText(value).replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
function readValue(id) {
|
||||
var el = getElement3(id);
|
||||
return el ? el.value.trim() : "";
|
||||
}
|
||||
function focusElement(id) {
|
||||
var el = getElement3(id);
|
||||
if (el) el.focus();
|
||||
}
|
||||
function setVisible(id, visible) {
|
||||
var el = getElement3(id);
|
||||
if (el) {
|
||||
el.style.display = visible ? "block" : "none";
|
||||
}
|
||||
}
|
||||
function setValue(id, value) {
|
||||
var el = getElement3(id);
|
||||
if (el) {
|
||||
el.value = value;
|
||||
}
|
||||
}
|
||||
function renderStatus(id, status) {
|
||||
var el = getElement3(id);
|
||||
if (!el) return;
|
||||
if (!status.visible) {
|
||||
el.textContent = "";
|
||||
el.className = "service-status";
|
||||
return;
|
||||
}
|
||||
el.textContent = status.message;
|
||||
el.className = "service-status visible" + (status.error ? " error" : " success");
|
||||
}
|
||||
function renderButton(id, disabled, label) {
|
||||
if (!id || !label) return;
|
||||
var button = getElement3(id);
|
||||
if (!button) return;
|
||||
button.disabled = disabled;
|
||||
button.textContent = label;
|
||||
}
|
||||
function renderOpenPanels(openPanelID) {
|
||||
var panels = ["manage-service-panel", "retrieve-service-panel", "refund-service-panel", "data-service-panel"];
|
||||
for (var i = 0; i < panels.length; i++) {
|
||||
var panel = getElement3(panels[i]);
|
||||
if (!panel) continue;
|
||||
panel.classList.toggle("visible", panels[i] === openPanelID);
|
||||
}
|
||||
}
|
||||
function renderRefundPanel(refundState, bootstrap) {
|
||||
var root = getElement3("refund-service-root");
|
||||
if (!root) return;
|
||||
var refundSupportURL = (bootstrap.public_site_url || "") + "/refund.html?email=" + encodeURIComponent(refundState.emailValue || "");
|
||||
root.innerHTML = '<h3>Refund requests</h3><p>Process an eligible self-serve refund for a self-hosted purchase. This revokes the associated license immediately.</p><div class="warning"><strong>Warning:</strong> completing a refund immediately revokes the affected license. This should only be used when the refund window and commercial contract allow it.</div><div class="form-group"><label for="refund-inline-email">Email address</label><input type="email" id="refund-inline-email" value="' + escapeAttribute(refundState.emailValue || "") + '" autocomplete="email" data-account-service-input="refund-email"></div><div class="form-group"><label for="refund-inline-token">License key</label><input type="text" id="refund-inline-token" value="' + escapeAttribute(refundState.tokenValue || "") + '" placeholder="pulse_xxxxx" data-account-service-input="refund-token"></div><div class="form-actions"><button class="btn-danger" type="button" id="refund-inline-submit" data-account-service-action="refund-inline-submit">Process Refund</button></div><div class="helper-text">If this purchase is not eligible for self-serve refund, use the public support path instead: <a href="' + escapeAttribute(refundSupportURL) + '">open refund support page</a>.</div><div class="service-status" id="refund-inline-status"></div>';
|
||||
}
|
||||
function renderManagePanel(flowState) {
|
||||
var root = getElement3("manage-service-root");
|
||||
if (!root) return;
|
||||
root.innerHTML = '<h3>Manage subscriptions</h3><p>Request a verification code for the commercial email, then open the Stripe customer portal for billing changes, invoices, and subscription actions.</p><div id="manage-inline-step1"><div class="form-group"><label for="manage-inline-email">Email address</label><input type="email" id="manage-inline-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="manage-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="manage-inline-request" data-account-service-action="manage-inline-request">Send Verification Code</button></div></div><div id="manage-inline-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="manage-inline-code">Verification code</label><input type="text" id="manage-inline-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="manage-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="manage-inline-confirm" data-account-service-action="manage-inline-confirm">Open Customer Portal</button></div><div class="helper-text">Need a new code? <a href="#" id="manage-inline-resend" data-account-service-action="manage-inline-resend">Send again</a></div></div><div class="service-status" id="manage-inline-status"></div>';
|
||||
}
|
||||
function renderRetrievePanel(flowState) {
|
||||
var root = getElement3("retrieve-service-root");
|
||||
if (!root) return;
|
||||
var result = flowState.result;
|
||||
var invoiceURL = result && result.invoice_url ? result.invoice_url : "#";
|
||||
var invoiceDisplay = result && result.invoice_url ? "inline-block" : "none";
|
||||
var copyDisplay = result ? "inline-block" : "none";
|
||||
var resultDisplay = result ? "block" : "none";
|
||||
root.innerHTML = '<h3>Retrieve licenses</h3><p>Request a verification code for the commercial email, then reveal the current active self-hosted license without leaving Pulse Account.</p><div id="retrieve-inline-step1"><div class="form-group"><label for="retrieve-inline-email">Email address</label><input type="email" id="retrieve-inline-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="retrieve-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="retrieve-inline-request" data-account-service-action="retrieve-inline-request">Send Verification Code</button></div></div><div id="retrieve-inline-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="retrieve-inline-code">Verification code</label><input type="text" id="retrieve-inline-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="retrieve-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="retrieve-inline-confirm" data-account-service-action="retrieve-inline-confirm">Show License</button><button class="btn-secondary" type="button" id="retrieve-inline-copy" data-account-service-action="retrieve-inline-copy" style="display:' + copyDisplay + '">Copy License Key</button><a class="btn-secondary" id="retrieve-inline-invoice" href="' + escapeAttribute(invoiceURL) + '" target="_blank" rel="noopener" style="display:' + invoiceDisplay + '">View Invoice</a></div><div class="helper-text">Use the latest active self-hosted license for this commercial email.</div></div><div class="service-status" id="retrieve-inline-status"></div><div id="retrieve-inline-result" style="display:' + resultDisplay + '; margin-top:14px"><label for="retrieve-inline-token">License key</label><textarea id="retrieve-inline-token" readonly>' + escapeText(result ? result.token : "") + '</textarea><div class="result-grid"><div><div class="result-meta-label">Plan</div><div class="result-meta-value" id="retrieve-inline-tier">' + escapeText(result ? result.tier : "") + '</div></div><div><div class="result-meta-label">Issued</div><div class="result-meta-value" id="retrieve-inline-issued">' + escapeText(result ? new Date(result.issued_at).toLocaleString() : "") + '</div></div><div><div class="result-meta-label">Expires</div><div class="result-meta-value" id="retrieve-inline-expires">' + escapeText(result ? result.expires_at ? new Date(result.expires_at).toLocaleString() : "Does not expire" : "") + '</div></div><div><div class="result-meta-label">Purchase Email</div><div class="result-meta-value" id="retrieve-inline-email-value">' + escapeText(result ? result.email : "") + "</div></div></div></div>";
|
||||
}
|
||||
function renderExportPanel(flowState) {
|
||||
var root = getElement3("data-export-root");
|
||||
if (!root) return;
|
||||
var resultDisplay = flowState.result ? "block" : "none";
|
||||
root.innerHTML = '<h4>Export My Data</h4><div id="data-export-step1"><div class="form-group"><label for="data-export-email">Email address</label><input type="email" id="data-export-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="data-export-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="data-export-request" data-account-service-action="data-export-request">Send Verification Code</button></div></div><div id="data-export-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="data-export-code">Verification code</label><input type="text" id="data-export-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-export-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="data-export-confirm" data-account-service-action="data-export-confirm">Export My Data</button></div><div class="helper-text">Need a new code? <a href="#" id="data-export-resend" data-account-service-action="data-export-resend">Send again</a></div></div><div class="service-status" id="data-export-status"></div><div id="data-export-result" style="display:' + resultDisplay + '; margin-top:14px"><label for="data-export-payload">Export payload</label><textarea id="data-export-payload" readonly>' + escapeText(flowState.result ? JSON.stringify(flowState.result, null, 2) : "") + "</textarea></div>";
|
||||
}
|
||||
function renderExportResult(result) {
|
||||
setVisible("data-export-result", !!result);
|
||||
setValue("data-export-payload", result ? JSON.stringify(result, null, 2) : "");
|
||||
}
|
||||
function renderDeletePanel(flowState) {
|
||||
var root = getElement3("data-delete-root");
|
||||
if (!root) return;
|
||||
root.innerHTML = '<h4>Delete My Data</h4><div class="warning"><strong>Warning:</strong> deleting commercial data also revokes license records and cannot be undone.</div><div id="data-delete-step1"><div class="form-group"><label for="data-delete-email">Email address</label><input type="email" id="data-delete-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="data-delete-email"></div><div class="form-actions"><button class="btn-danger" type="button" id="data-delete-request" data-account-service-action="data-delete-request">Send Verification Code</button></div></div><div id="data-delete-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="data-delete-code">Verification code</label><input type="text" id="data-delete-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-delete-code"></div><div class="checkbox-row"><input type="checkbox" id="data-delete-confirm-check"' + (flowState.checkboxChecked ? " checked" : "") + '><span>I understand this permanently deletes my commercial data and revokes associated licenses.</span></div><div class="form-actions"><button class="btn-danger" type="button" id="data-delete-confirm" data-account-service-action="data-delete-confirm">Delete My Data</button></div><div class="helper-text">Need a new code? <a href="#" id="data-delete-resend" data-account-service-action="data-delete-resend">Send again</a></div></div><div class="service-status" id="data-delete-status"></div>';
|
||||
}
|
||||
|
||||
// src/services_controller.ts
|
||||
function installServicesController(deps) {
|
||||
document.addEventListener("click", function(event) {
|
||||
var target = asHTMLElement3(event.target)?.closest("[data-account-service-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") || "";
|
||||
switch (action) {
|
||||
case "open-service-panel":
|
||||
event.preventDefault();
|
||||
deps.toggleServicePanel(panelID);
|
||||
deps.focusElement(focusID);
|
||||
return;
|
||||
case "manage-inline-request":
|
||||
event.preventDefault();
|
||||
deps.requestVerificationCode("manage");
|
||||
return;
|
||||
case "manage-inline-resend":
|
||||
deps.resendVerificationCode("manage", event);
|
||||
return;
|
||||
case "manage-inline-confirm":
|
||||
event.preventDefault();
|
||||
deps.confirmVerificationCode("manage");
|
||||
return;
|
||||
case "retrieve-inline-request":
|
||||
event.preventDefault();
|
||||
deps.requestVerificationCode("retrieve");
|
||||
return;
|
||||
case "retrieve-inline-confirm":
|
||||
event.preventDefault();
|
||||
deps.confirmVerificationCode("retrieve");
|
||||
return;
|
||||
case "retrieve-inline-copy":
|
||||
event.preventDefault();
|
||||
deps.copyRetrievedLicense();
|
||||
return;
|
||||
case "refund-inline-submit":
|
||||
event.preventDefault();
|
||||
deps.submitRefund();
|
||||
return;
|
||||
case "data-export-request":
|
||||
event.preventDefault();
|
||||
deps.requestVerificationCode("export");
|
||||
return;
|
||||
case "data-export-resend":
|
||||
deps.resendVerificationCode("export", event);
|
||||
return;
|
||||
case "data-export-confirm":
|
||||
event.preventDefault();
|
||||
deps.confirmVerificationCode("export");
|
||||
return;
|
||||
case "data-delete-request":
|
||||
event.preventDefault();
|
||||
deps.requestVerificationCode("delete");
|
||||
return;
|
||||
case "data-delete-resend":
|
||||
deps.resendVerificationCode("delete", event);
|
||||
return;
|
||||
case "data-delete-confirm":
|
||||
event.preventDefault();
|
||||
deps.confirmVerificationCode("delete");
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
document.addEventListener("input", function(event) {
|
||||
var target = asHTMLElement3(event.target);
|
||||
if (!target) return;
|
||||
var inputKind = target.getAttribute("data-account-service-input") || "";
|
||||
if (!inputKind) return;
|
||||
deps.updateInputValue(inputKind, target.value);
|
||||
});
|
||||
document.addEventListener("change", function(event) {
|
||||
var target = asHTMLElement3(event.target);
|
||||
if (!target || target.id !== "data-delete-confirm-check") return;
|
||||
deps.updateDeleteConfirmation(!!target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
// src/services.ts
|
||||
var serviceState = {
|
||||
openPanelID: "",
|
||||
|
|
@ -752,38 +929,6 @@
|
|||
function getCommercialAPIBaseURL2() {
|
||||
return getCommercialAPIBaseURL();
|
||||
}
|
||||
function getElement3(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
function asHTMLElement3(target) {
|
||||
return target instanceof HTMLElement ? target : null;
|
||||
}
|
||||
function escapeText(value) {
|
||||
return String(value || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
function escapeAttribute(value) {
|
||||
return escapeText(value).replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
function readValue(id) {
|
||||
var el = getElement3(id);
|
||||
return el ? el.value.trim() : "";
|
||||
}
|
||||
function focusElement(id) {
|
||||
var el = getElement3(id);
|
||||
if (el) el.focus();
|
||||
}
|
||||
function setVisible(id, visible) {
|
||||
var el = getElement3(id);
|
||||
if (el) {
|
||||
el.style.display = visible ? "block" : "none";
|
||||
}
|
||||
}
|
||||
function setValue(id, value) {
|
||||
var el = getElement3(id);
|
||||
if (el) {
|
||||
el.value = value;
|
||||
}
|
||||
}
|
||||
function serviceFetch(path, body) {
|
||||
return fetch(getCommercialAPIBaseURL2() + path, {
|
||||
method: "POST",
|
||||
|
|
@ -808,34 +953,9 @@
|
|||
error: !!isError
|
||||
};
|
||||
}
|
||||
function renderStatus(id, status) {
|
||||
var el = getElement3(id);
|
||||
if (!el) return;
|
||||
if (!status.visible) {
|
||||
el.textContent = "";
|
||||
el.className = "service-status";
|
||||
return;
|
||||
}
|
||||
el.textContent = status.message;
|
||||
el.className = "service-status visible" + (status.error ? " error" : " success");
|
||||
}
|
||||
function renderButton(id, disabled, label) {
|
||||
var button = getElement3(id);
|
||||
if (!button) return;
|
||||
button.disabled = disabled;
|
||||
button.textContent = label;
|
||||
}
|
||||
function toggleServicePanel(panelID) {
|
||||
serviceState.openPanelID = serviceState.openPanelID === panelID ? "" : panelID;
|
||||
renderOpenPanels();
|
||||
}
|
||||
function renderOpenPanels() {
|
||||
var panels = ["manage-service-panel", "retrieve-service-panel", "refund-service-panel", "data-service-panel"];
|
||||
for (var i = 0; i < panels.length; i++) {
|
||||
var panel = getElement3(panels[i]);
|
||||
if (!panel) continue;
|
||||
panel.classList.toggle("visible", panels[i] === serviceState.openPanelID);
|
||||
}
|
||||
renderOpenPanels(serviceState.openPanelID);
|
||||
}
|
||||
function renderFlow(flowID) {
|
||||
var flow = verificationFlows[flowID];
|
||||
|
|
@ -862,17 +982,10 @@
|
|||
renderRefund();
|
||||
}
|
||||
function renderRefund() {
|
||||
renderRefundPanel();
|
||||
renderRefundPanel(serviceState.refund, getBootstrap());
|
||||
renderButton("refund-inline-submit", serviceState.refund.submitting, serviceState.refund.submitting ? "Processing..." : "Process Refund");
|
||||
renderStatus("refund-inline-status", serviceState.refund.status);
|
||||
}
|
||||
function renderRefundPanel() {
|
||||
var root = getElement3("refund-service-root");
|
||||
if (!root) return;
|
||||
var bootstrap = getBootstrap();
|
||||
var refundSupportURL = (bootstrap.public_site_url || "") + "/refund.html?email=" + encodeURIComponent(serviceState.refund.emailValue || "");
|
||||
root.innerHTML = '<h3>Refund requests</h3><p>Process an eligible self-serve refund for a self-hosted purchase. This revokes the associated license immediately.</p><div class="warning"><strong>Warning:</strong> completing a refund immediately revokes the affected license. This should only be used when the refund window and commercial contract allow it.</div><div class="form-group"><label for="refund-inline-email">Email address</label><input type="email" id="refund-inline-email" value="' + escapeAttribute(serviceState.refund.emailValue || "") + '" autocomplete="email" data-account-service-input="refund-email"></div><div class="form-group"><label for="refund-inline-token">License key</label><input type="text" id="refund-inline-token" value="' + escapeAttribute(serviceState.refund.tokenValue || "") + '" placeholder="pulse_xxxxx" data-account-service-input="refund-token"></div><div class="form-actions"><button class="btn-danger" type="button" id="refund-inline-submit" data-account-service-action="refund-inline-submit">Process Refund</button></div><div class="helper-text">If this purchase is not eligible for self-serve refund, use the public support path instead: <a href="' + escapeAttribute(refundSupportURL) + '">open refund support page</a>.</div><div class="service-status" id="refund-inline-status"></div>';
|
||||
}
|
||||
function resetVerificationFlow(flowID) {
|
||||
var flow = verificationFlows[flowID];
|
||||
if (!flow) return;
|
||||
|
|
@ -913,11 +1026,7 @@
|
|||
onConfirmSuccess: function(data) {
|
||||
window.location.href = data.url;
|
||||
},
|
||||
renderPanel: function(flowState) {
|
||||
var root = getElement3("manage-service-root");
|
||||
if (!root) return;
|
||||
root.innerHTML = '<h3>Manage subscriptions</h3><p>Request a verification code for the commercial email, then open the Stripe customer portal for billing changes, invoices, and subscription actions.</p><div id="manage-inline-step1"><div class="form-group"><label for="manage-inline-email">Email address</label><input type="email" id="manage-inline-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="manage-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="manage-inline-request" data-account-service-action="manage-inline-request">Send Verification Code</button></div></div><div id="manage-inline-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="manage-inline-code">Verification code</label><input type="text" id="manage-inline-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="manage-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="manage-inline-confirm" data-account-service-action="manage-inline-confirm">Open Customer Portal</button></div><div class="helper-text">Need a new code? <a href="#" id="manage-inline-resend" data-account-service-action="manage-inline-resend">Send again</a></div></div><div class="service-status" id="manage-inline-status"></div>';
|
||||
}
|
||||
renderPanel: renderManagePanel
|
||||
},
|
||||
retrieve: {
|
||||
requestPath: "/v1/retrieve-license/request",
|
||||
|
|
@ -951,19 +1060,7 @@
|
|||
serviceState.flows.retrieve.codeValue = "";
|
||||
setFlowStatus("retrieve", "License retrieved successfully.", false);
|
||||
},
|
||||
renderPanel: function(flowState) {
|
||||
var root = getElement3("retrieve-service-root");
|
||||
if (!root) return;
|
||||
var result = flowState.result;
|
||||
var invoiceURL = result && result.invoice_url ? result.invoice_url : "#";
|
||||
var invoiceDisplay = result && result.invoice_url ? "inline-block" : "none";
|
||||
var copyDisplay = result ? "inline-block" : "none";
|
||||
var resultDisplay = result ? "block" : "none";
|
||||
root.innerHTML = '<h3>Retrieve licenses</h3><p>Request a verification code for the commercial email, then reveal the current active self-hosted license without leaving Pulse Account.</p><div id="retrieve-inline-step1"><div class="form-group"><label for="retrieve-inline-email">Email address</label><input type="email" id="retrieve-inline-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="retrieve-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="retrieve-inline-request" data-account-service-action="retrieve-inline-request">Send Verification Code</button></div></div><div id="retrieve-inline-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="retrieve-inline-code">Verification code</label><input type="text" id="retrieve-inline-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="retrieve-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="retrieve-inline-confirm" data-account-service-action="retrieve-inline-confirm">Show License</button><button class="btn-secondary" type="button" id="retrieve-inline-copy" data-account-service-action="retrieve-inline-copy" style="display:' + copyDisplay + '">Copy License Key</button><a class="btn-secondary" id="retrieve-inline-invoice" href="' + escapeAttribute(invoiceURL) + '" target="_blank" rel="noopener" style="display:' + invoiceDisplay + '">View Invoice</a></div><div class="helper-text">Use the latest active self-hosted license for this commercial email.</div></div><div class="service-status" id="retrieve-inline-status"></div><div id="retrieve-inline-result" style="display:' + resultDisplay + '; margin-top:14px"><label for="retrieve-inline-token">License key</label><textarea id="retrieve-inline-token" readonly>' + escapeText(result ? result.token : "") + '</textarea><div class="result-grid"><div><div class="result-meta-label">Plan</div><div class="result-meta-value" id="retrieve-inline-tier">' + escapeText(result ? result.tier : "") + '</div></div><div><div class="result-meta-label">Issued</div><div class="result-meta-value" id="retrieve-inline-issued">' + escapeText(result ? new Date(result.issued_at).toLocaleString() : "") + '</div></div><div><div class="result-meta-label">Expires</div><div class="result-meta-value" id="retrieve-inline-expires">' + escapeText(result ? result.expires_at ? new Date(result.expires_at).toLocaleString() : "Does not expire" : "") + '</div></div><div><div class="result-meta-label">Purchase Email</div><div class="result-meta-value" id="retrieve-inline-email-value">' + escapeText(result ? result.email : "") + "</div></div></div></div>";
|
||||
},
|
||||
renderResult: function(result) {
|
||||
void result;
|
||||
}
|
||||
renderPanel: renderRetrievePanel
|
||||
},
|
||||
export: {
|
||||
requestPath: "/v1/gdpr/request-export",
|
||||
|
|
@ -999,16 +1096,8 @@
|
|||
resetVerificationFlow("export");
|
||||
serviceState.flows.export.result = data;
|
||||
},
|
||||
renderPanel: function(flowState) {
|
||||
var root = getElement3("data-export-root");
|
||||
if (!root) return;
|
||||
var resultDisplay = flowState.result ? "block" : "none";
|
||||
root.innerHTML = '<h4>Export My Data</h4><div id="data-export-step1"><div class="form-group"><label for="data-export-email">Email address</label><input type="email" id="data-export-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="data-export-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="data-export-request" data-account-service-action="data-export-request">Send Verification Code</button></div></div><div id="data-export-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="data-export-code">Verification code</label><input type="text" id="data-export-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-export-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="data-export-confirm" data-account-service-action="data-export-confirm">Export My Data</button></div><div class="helper-text">Need a new code? <a href="#" id="data-export-resend" data-account-service-action="data-export-resend">Send again</a></div></div><div class="service-status" id="data-export-status"></div><div id="data-export-result" style="display:' + resultDisplay + '; margin-top:14px"><label for="data-export-payload">Export payload</label><textarea id="data-export-payload" readonly>' + escapeText(flowState.result ? JSON.stringify(flowState.result, null, 2) : "") + "</textarea></div>";
|
||||
},
|
||||
renderResult: function(result) {
|
||||
setVisible("data-export-result", !!result);
|
||||
setValue("data-export-payload", result ? JSON.stringify(result, null, 2) : "");
|
||||
}
|
||||
renderPanel: renderExportPanel,
|
||||
renderResult: renderExportResult
|
||||
},
|
||||
delete: {
|
||||
requestPath: "/v1/gdpr/request-delete",
|
||||
|
|
@ -1050,11 +1139,7 @@
|
|||
resetVerificationFlow("delete");
|
||||
setFlowStatus("delete", data.deleted_count > 0 && data.stripe_reminder ? data.message + " " + data.stripe_reminder : data.message, false);
|
||||
},
|
||||
renderPanel: function(flowState) {
|
||||
var root = getElement3("data-delete-root");
|
||||
if (!root) return;
|
||||
root.innerHTML = '<h4>Delete My Data</h4><div class="warning"><strong>Warning:</strong> deleting commercial data also revokes license records and cannot be undone.</div><div id="data-delete-step1"><div class="form-group"><label for="data-delete-email">Email address</label><input type="email" id="data-delete-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="data-delete-email"></div><div class="form-actions"><button class="btn-danger" type="button" id="data-delete-request" data-account-service-action="data-delete-request">Send Verification Code</button></div></div><div id="data-delete-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="data-delete-code">Verification code</label><input type="text" id="data-delete-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-delete-code"></div><div class="checkbox-row"><input type="checkbox" id="data-delete-confirm-check"' + (flowState.checkboxChecked ? " checked" : "") + '><span>I understand this permanently deletes my commercial data and revokes associated licenses.</span></div><div class="form-actions"><button class="btn-danger" type="button" id="data-delete-confirm" data-account-service-action="data-delete-confirm">Delete My Data</button></div><div class="helper-text">Need a new code? <a href="#" id="data-delete-resend" data-account-service-action="data-delete-resend">Send again</a></div></div><div class="service-status" id="data-delete-status"></div>';
|
||||
}
|
||||
renderPanel: renderDeletePanel
|
||||
}
|
||||
};
|
||||
async function requestVerificationCode(flowID) {
|
||||
|
|
@ -1170,118 +1255,68 @@
|
|||
}
|
||||
function renderServiceRuntime() {
|
||||
syncServiceStateFromBootstrap();
|
||||
renderOpenPanels();
|
||||
renderOpenPanels(serviceState.openPanelID);
|
||||
renderAllFlows();
|
||||
}
|
||||
renderServiceRuntime();
|
||||
subscribePortalRender(renderServiceRuntime);
|
||||
document.addEventListener("click", function(event) {
|
||||
var target = asHTMLElement3(event.target)?.closest("[data-account-service-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") || "";
|
||||
switch (action) {
|
||||
case "open-service-panel":
|
||||
event.preventDefault();
|
||||
toggleServicePanel(panelID);
|
||||
focusElement(focusID);
|
||||
return;
|
||||
case "manage-inline-request":
|
||||
event.preventDefault();
|
||||
requestVerificationCode("manage");
|
||||
return;
|
||||
case "manage-inline-resend":
|
||||
resendVerificationCode("manage", event);
|
||||
return;
|
||||
case "manage-inline-confirm":
|
||||
event.preventDefault();
|
||||
confirmVerificationCode("manage");
|
||||
return;
|
||||
case "retrieve-inline-request":
|
||||
event.preventDefault();
|
||||
requestVerificationCode("retrieve");
|
||||
return;
|
||||
case "retrieve-inline-confirm":
|
||||
event.preventDefault();
|
||||
confirmVerificationCode("retrieve");
|
||||
return;
|
||||
case "retrieve-inline-copy":
|
||||
event.preventDefault();
|
||||
copyRetrievedLicense();
|
||||
return;
|
||||
case "refund-inline-submit":
|
||||
event.preventDefault();
|
||||
submitRefund();
|
||||
return;
|
||||
case "data-export-request":
|
||||
event.preventDefault();
|
||||
requestVerificationCode("export");
|
||||
return;
|
||||
case "data-export-resend":
|
||||
resendVerificationCode("export", event);
|
||||
return;
|
||||
case "data-export-confirm":
|
||||
event.preventDefault();
|
||||
confirmVerificationCode("export");
|
||||
return;
|
||||
case "data-delete-request":
|
||||
event.preventDefault();
|
||||
requestVerificationCode("delete");
|
||||
return;
|
||||
case "data-delete-resend":
|
||||
resendVerificationCode("delete", event);
|
||||
return;
|
||||
case "data-delete-confirm":
|
||||
event.preventDefault();
|
||||
confirmVerificationCode("delete");
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
document.addEventListener("input", function(event) {
|
||||
var target = asHTMLElement3(event.target);
|
||||
if (!target) return;
|
||||
var inputKind = target.getAttribute("data-account-service-input") || "";
|
||||
function updateInputValue(inputKind, value) {
|
||||
switch (inputKind) {
|
||||
case "manage-email":
|
||||
serviceState.flows.manage.emailValue = target.value;
|
||||
serviceState.flows.manage.emailValue = value;
|
||||
return;
|
||||
case "manage-code":
|
||||
serviceState.flows.manage.codeValue = target.value;
|
||||
serviceState.flows.manage.codeValue = value;
|
||||
return;
|
||||
case "retrieve-email":
|
||||
serviceState.flows.retrieve.emailValue = target.value;
|
||||
serviceState.flows.retrieve.emailValue = value;
|
||||
return;
|
||||
case "retrieve-code":
|
||||
serviceState.flows.retrieve.codeValue = target.value;
|
||||
serviceState.flows.retrieve.codeValue = value;
|
||||
return;
|
||||
case "refund-email":
|
||||
serviceState.refund.emailValue = target.value;
|
||||
serviceState.refund.emailValue = value;
|
||||
return;
|
||||
case "refund-token":
|
||||
serviceState.refund.tokenValue = target.value;
|
||||
serviceState.refund.tokenValue = value;
|
||||
return;
|
||||
case "data-export-email":
|
||||
serviceState.flows.export.emailValue = target.value;
|
||||
serviceState.flows.export.emailValue = value;
|
||||
return;
|
||||
case "data-export-code":
|
||||
serviceState.flows.export.codeValue = target.value;
|
||||
serviceState.flows.export.codeValue = value;
|
||||
return;
|
||||
case "data-delete-email":
|
||||
serviceState.flows.delete.emailValue = target.value;
|
||||
serviceState.flows.delete.emailValue = value;
|
||||
return;
|
||||
case "data-delete-code":
|
||||
serviceState.flows.delete.codeValue = target.value;
|
||||
serviceState.flows.delete.codeValue = value;
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
document.addEventListener("change", function(event) {
|
||||
var target = asHTMLElement3(event.target);
|
||||
if (!target || target.id !== "data-delete-confirm-check") return;
|
||||
serviceState.flows.delete.checkboxChecked = !!target.checked;
|
||||
}
|
||||
installServicesController({
|
||||
toggleServicePanel,
|
||||
focusElement,
|
||||
requestVerificationCode: function(flowID) {
|
||||
void requestVerificationCode(flowID);
|
||||
},
|
||||
resendVerificationCode: function(flowID, event) {
|
||||
void resendVerificationCode(flowID, event);
|
||||
},
|
||||
confirmVerificationCode: function(flowID) {
|
||||
void confirmVerificationCode(flowID);
|
||||
},
|
||||
copyRetrievedLicense: function() {
|
||||
void copyRetrievedLicense();
|
||||
},
|
||||
submitRefund: function() {
|
||||
void submitRefund();
|
||||
},
|
||||
updateInputValue,
|
||||
updateDeleteConfirmation: function(checked) {
|
||||
serviceState.flows.delete.checkboxChecked = checked;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ const buildInputs = [
|
|||
'src/shell.ts',
|
||||
'src/shell_view.ts',
|
||||
'src/services.ts',
|
||||
'src/services_controller.ts',
|
||||
'src/services_view.ts',
|
||||
'src/runtime.ts',
|
||||
'src/types.ts',
|
||||
'src/styles.css',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,21 @@
|
|||
import { getBootstrap, getCommercialAPIBaseURL as readCommercialAPIBaseURL, subscribePortalRender } from './runtime';
|
||||
import { installServicesController } from './services_controller';
|
||||
import {
|
||||
focusElement,
|
||||
getElement,
|
||||
readValue,
|
||||
renderButton,
|
||||
renderDeletePanel,
|
||||
renderExportPanel,
|
||||
renderExportResult,
|
||||
renderManagePanel,
|
||||
renderOpenPanels,
|
||||
renderRefundPanel,
|
||||
renderRetrievePanel,
|
||||
renderStatus,
|
||||
setValue,
|
||||
setVisible,
|
||||
} from './services_view';
|
||||
import type { RefundState, ServiceStatus, VerificationFlowState } from './types';
|
||||
|
||||
type FlowID = 'manage' | 'retrieve' | 'export' | 'delete';
|
||||
|
|
@ -30,8 +47,6 @@ interface VerificationFlowDefinition {
|
|||
renderResult?: (result: unknown) => void;
|
||||
}
|
||||
|
||||
type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
|
||||
var serviceState = {
|
||||
openPanelID: '',
|
||||
flows: {
|
||||
|
|
@ -74,58 +89,6 @@ type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectEleme
|
|||
return readCommercialAPIBaseURL();
|
||||
}
|
||||
|
||||
function getElement<T extends HTMLElement = HTMLElement>(id): T | null {
|
||||
return document.getElementById(id) as T | null;
|
||||
}
|
||||
|
||||
function asHTMLElement(target: EventTarget | null): HTMLElement | null {
|
||||
return target instanceof HTMLElement ? target : null;
|
||||
}
|
||||
|
||||
function escapeText(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function escapeAttribute(value) {
|
||||
return escapeText(value)
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function readValue(id) {
|
||||
var el = getElement<FormValueElement>(id);
|
||||
return el ? el.value.trim() : '';
|
||||
}
|
||||
|
||||
function focusElement(id) {
|
||||
var el = getElement<FormValueElement>(id);
|
||||
if (el) el.focus();
|
||||
}
|
||||
|
||||
function setVisible(id, visible) {
|
||||
var el = getElement(id);
|
||||
if (el) {
|
||||
el.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
var el = getElement(id);
|
||||
if (el) {
|
||||
el.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setValue(id, value) {
|
||||
var el = getElement<FormValueElement>(id);
|
||||
if (el) {
|
||||
el.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function serviceFetch(path, body) {
|
||||
return fetch(getCommercialAPIBaseURL() + path, {
|
||||
method: 'POST',
|
||||
|
|
@ -154,37 +117,9 @@ type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectEleme
|
|||
};
|
||||
}
|
||||
|
||||
function renderStatus(id, status) {
|
||||
var el = getElement(id);
|
||||
if (!el) return;
|
||||
if (!status.visible) {
|
||||
el.textContent = '';
|
||||
el.className = 'service-status';
|
||||
return;
|
||||
}
|
||||
el.textContent = status.message;
|
||||
el.className = 'service-status visible' + (status.error ? ' error' : ' success');
|
||||
}
|
||||
|
||||
function renderButton(id, disabled, label) {
|
||||
var button = getElement<HTMLButtonElement>(id);
|
||||
if (!button) return;
|
||||
button.disabled = disabled;
|
||||
button.textContent = label;
|
||||
}
|
||||
|
||||
function toggleServicePanel(panelID) {
|
||||
serviceState.openPanelID = serviceState.openPanelID === panelID ? '' : panelID;
|
||||
renderOpenPanels();
|
||||
}
|
||||
|
||||
function renderOpenPanels() {
|
||||
var panels = ['manage-service-panel', 'retrieve-service-panel', 'refund-service-panel', 'data-service-panel'];
|
||||
for (var i = 0; i < panels.length; i++) {
|
||||
var panel = getElement(panels[i]);
|
||||
if (!panel) continue;
|
||||
panel.classList.toggle('visible', panels[i] === serviceState.openPanelID);
|
||||
}
|
||||
renderOpenPanels(serviceState.openPanelID);
|
||||
}
|
||||
|
||||
function renderFlow(flowID) {
|
||||
|
|
@ -214,35 +149,11 @@ type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectEleme
|
|||
}
|
||||
|
||||
function renderRefund() {
|
||||
renderRefundPanel();
|
||||
renderRefundPanel(serviceState.refund, getBootstrap());
|
||||
renderButton('refund-inline-submit', serviceState.refund.submitting, serviceState.refund.submitting ? 'Processing...' : 'Process Refund');
|
||||
renderStatus('refund-inline-status', serviceState.refund.status);
|
||||
}
|
||||
|
||||
function renderRefundPanel() {
|
||||
var root = getElement('refund-service-root');
|
||||
if (!root) return;
|
||||
var bootstrap = getBootstrap();
|
||||
var refundSupportURL = (bootstrap.public_site_url || '') + '/refund.html?email=' + encodeURIComponent(serviceState.refund.emailValue || '');
|
||||
root.innerHTML = '' +
|
||||
'<h3>Refund requests</h3>' +
|
||||
'<p>Process an eligible self-serve refund for a self-hosted purchase. This revokes the associated license immediately.</p>' +
|
||||
'<div class="warning"><strong>Warning:</strong> completing a refund immediately revokes the affected license. This should only be used when the refund window and commercial contract allow it.</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="refund-inline-email">Email address</label>' +
|
||||
'<input type="email" id="refund-inline-email" value="' + escapeAttribute(serviceState.refund.emailValue || '') + '" autocomplete="email" data-account-service-input="refund-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="refund-inline-token">License key</label>' +
|
||||
'<input type="text" id="refund-inline-token" value="' + escapeAttribute(serviceState.refund.tokenValue || '') + '" placeholder="pulse_xxxxx" data-account-service-input="refund-token">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-danger" type="button" id="refund-inline-submit" data-account-service-action="refund-inline-submit">Process Refund</button>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">If this purchase is not eligible for self-serve refund, use the public support path instead: <a href="' + escapeAttribute(refundSupportURL) + '">open refund support page</a>.</div>' +
|
||||
'<div class="service-status" id="refund-inline-status"></div>';
|
||||
}
|
||||
|
||||
function resetVerificationFlow(flowID: FlowID) {
|
||||
var flow = verificationFlows[flowID];
|
||||
if (!flow) return;
|
||||
|
|
@ -283,33 +194,7 @@ type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectEleme
|
|||
onConfirmSuccess: function(data) {
|
||||
window.location.href = data.url;
|
||||
},
|
||||
renderPanel: function(flowState) {
|
||||
var root = getElement('manage-service-root');
|
||||
if (!root) return;
|
||||
root.innerHTML = '' +
|
||||
'<h3>Manage subscriptions</h3>' +
|
||||
'<p>Request a verification code for the commercial email, then open the Stripe customer portal for billing changes, invoices, and subscription actions.</p>' +
|
||||
'<div id="manage-inline-step1">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="manage-inline-email">Email address</label>' +
|
||||
'<input type="email" id="manage-inline-email" value="' + escapeAttribute(flowState.emailValue || '') + '" autocomplete="email" data-account-service-input="manage-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="manage-inline-request" data-account-service-action="manage-inline-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="manage-inline-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="manage-inline-code">Verification code</label>' +
|
||||
'<input type="text" id="manage-inline-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="manage-code">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="manage-inline-confirm" data-account-service-action="manage-inline-confirm">Open Customer Portal</button>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Need a new code? <a href="#" id="manage-inline-resend" data-account-service-action="manage-inline-resend">Send again</a></div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="manage-inline-status"></div>';
|
||||
}
|
||||
renderPanel: renderManagePanel
|
||||
},
|
||||
retrieve: {
|
||||
requestPath: '/v1/retrieve-license/request',
|
||||
|
|
@ -343,60 +228,7 @@ type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectEleme
|
|||
serviceState.flows.retrieve.codeValue = '';
|
||||
setFlowStatus('retrieve', 'License retrieved successfully.', false);
|
||||
},
|
||||
renderPanel: function(flowState) {
|
||||
var root = getElement('retrieve-service-root');
|
||||
if (!root) return;
|
||||
var result = flowState.result as {
|
||||
invoice_url?: string;
|
||||
token?: string;
|
||||
tier?: string;
|
||||
issued_at?: string;
|
||||
expires_at?: string | null;
|
||||
email?: string;
|
||||
} | null;
|
||||
var invoiceURL = result && result.invoice_url ? result.invoice_url : '#';
|
||||
var invoiceDisplay = result && result.invoice_url ? 'inline-block' : 'none';
|
||||
var copyDisplay = result ? 'inline-block' : 'none';
|
||||
var resultDisplay = result ? 'block' : 'none';
|
||||
root.innerHTML = '' +
|
||||
'<h3>Retrieve licenses</h3>' +
|
||||
'<p>Request a verification code for the commercial email, then reveal the current active self-hosted license without leaving Pulse Account.</p>' +
|
||||
'<div id="retrieve-inline-step1">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="retrieve-inline-email">Email address</label>' +
|
||||
'<input type="email" id="retrieve-inline-email" value="' + escapeAttribute(flowState.emailValue || '') + '" autocomplete="email" data-account-service-input="retrieve-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="retrieve-inline-request" data-account-service-action="retrieve-inline-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="retrieve-inline-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="retrieve-inline-code">Verification code</label>' +
|
||||
'<input type="text" id="retrieve-inline-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="retrieve-code">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="retrieve-inline-confirm" data-account-service-action="retrieve-inline-confirm">Show License</button>' +
|
||||
'<button class="btn-secondary" type="button" id="retrieve-inline-copy" data-account-service-action="retrieve-inline-copy" style="display:' + copyDisplay + '">Copy License Key</button>' +
|
||||
'<a class="btn-secondary" id="retrieve-inline-invoice" href="' + escapeAttribute(invoiceURL) + '" target="_blank" rel="noopener" style="display:' + invoiceDisplay + '">View Invoice</a>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Use the latest active self-hosted license for this commercial email.</div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="retrieve-inline-status"></div>' +
|
||||
'<div id="retrieve-inline-result" style="display:' + resultDisplay + '; margin-top:14px">' +
|
||||
'<label for="retrieve-inline-token">License key</label>' +
|
||||
'<textarea id="retrieve-inline-token" readonly>' + escapeText(result ? result.token : '') + '</textarea>' +
|
||||
'<div class="result-grid">' +
|
||||
'<div><div class="result-meta-label">Plan</div><div class="result-meta-value" id="retrieve-inline-tier">' + escapeText(result ? result.tier : '') + '</div></div>' +
|
||||
'<div><div class="result-meta-label">Issued</div><div class="result-meta-value" id="retrieve-inline-issued">' + escapeText(result ? new Date(result.issued_at).toLocaleString() : '') + '</div></div>' +
|
||||
'<div><div class="result-meta-label">Expires</div><div class="result-meta-value" id="retrieve-inline-expires">' + escapeText(result ? (result.expires_at ? new Date(result.expires_at).toLocaleString() : 'Does not expire') : '') + '</div></div>' +
|
||||
'<div><div class="result-meta-label">Purchase Email</div><div class="result-meta-value" id="retrieve-inline-email-value">' + escapeText(result ? result.email : '') + '</div></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
},
|
||||
renderResult: function(result) {
|
||||
void result;
|
||||
}
|
||||
renderPanel: renderRetrievePanel
|
||||
},
|
||||
export: {
|
||||
requestPath: '/v1/gdpr/request-export',
|
||||
|
|
@ -432,41 +264,8 @@ type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectEleme
|
|||
resetVerificationFlow('export');
|
||||
serviceState.flows.export.result = data;
|
||||
},
|
||||
renderPanel: function(flowState) {
|
||||
var root = getElement('data-export-root');
|
||||
if (!root) return;
|
||||
var resultDisplay = flowState.result ? 'block' : 'none';
|
||||
root.innerHTML = '' +
|
||||
'<h4>Export My Data</h4>' +
|
||||
'<div id="data-export-step1">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-export-email">Email address</label>' +
|
||||
'<input type="email" id="data-export-email" value="' + escapeAttribute(flowState.emailValue || '') + '" autocomplete="email" data-account-service-input="data-export-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="data-export-request" data-account-service-action="data-export-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="data-export-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-export-code">Verification code</label>' +
|
||||
'<input type="text" id="data-export-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-export-code">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="data-export-confirm" data-account-service-action="data-export-confirm">Export My Data</button>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Need a new code? <a href="#" id="data-export-resend" data-account-service-action="data-export-resend">Send again</a></div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="data-export-status"></div>' +
|
||||
'<div id="data-export-result" style="display:' + resultDisplay + '; margin-top:14px">' +
|
||||
'<label for="data-export-payload">Export payload</label>' +
|
||||
'<textarea id="data-export-payload" readonly>' + escapeText(flowState.result ? JSON.stringify(flowState.result, null, 2) : '') + '</textarea>' +
|
||||
'</div>';
|
||||
},
|
||||
renderResult: function(result) {
|
||||
setVisible('data-export-result', !!result);
|
||||
setValue('data-export-payload', result ? JSON.stringify(result, null, 2) : '');
|
||||
}
|
||||
renderPanel: renderExportPanel,
|
||||
renderResult: renderExportResult
|
||||
},
|
||||
delete: {
|
||||
requestPath: '/v1/gdpr/request-delete',
|
||||
|
|
@ -508,37 +307,7 @@ type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectEleme
|
|||
resetVerificationFlow('delete');
|
||||
setFlowStatus('delete', data.deleted_count > 0 && data.stripe_reminder ? data.message + ' ' + data.stripe_reminder : data.message, false);
|
||||
},
|
||||
renderPanel: function(flowState) {
|
||||
var root = getElement('data-delete-root');
|
||||
if (!root) return;
|
||||
root.innerHTML = '' +
|
||||
'<h4>Delete My Data</h4>' +
|
||||
'<div class="warning"><strong>Warning:</strong> deleting commercial data also revokes license records and cannot be undone.</div>' +
|
||||
'<div id="data-delete-step1">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-delete-email">Email address</label>' +
|
||||
'<input type="email" id="data-delete-email" value="' + escapeAttribute(flowState.emailValue || '') + '" autocomplete="email" data-account-service-input="data-delete-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-danger" type="button" id="data-delete-request" data-account-service-action="data-delete-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="data-delete-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-delete-code">Verification code</label>' +
|
||||
'<input type="text" id="data-delete-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-delete-code">' +
|
||||
'</div>' +
|
||||
'<div class="checkbox-row">' +
|
||||
'<input type="checkbox" id="data-delete-confirm-check"' + (flowState.checkboxChecked ? ' checked' : '') + '>' +
|
||||
'<span>I understand this permanently deletes my commercial data and revokes associated licenses.</span>' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-danger" type="button" id="data-delete-confirm" data-account-service-action="data-delete-confirm">Delete My Data</button>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Need a new code? <a href="#" id="data-delete-resend" data-account-service-action="data-delete-resend">Send again</a></div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="data-delete-status"></div>';
|
||||
}
|
||||
renderPanel: renderDeletePanel
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -661,122 +430,70 @@ type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectEleme
|
|||
|
||||
function renderServiceRuntime() {
|
||||
syncServiceStateFromBootstrap();
|
||||
renderOpenPanels();
|
||||
renderOpenPanels(serviceState.openPanelID);
|
||||
renderAllFlows();
|
||||
}
|
||||
|
||||
renderServiceRuntime();
|
||||
subscribePortalRender(renderServiceRuntime);
|
||||
|
||||
document.addEventListener('click', function(event) {
|
||||
var target = asHTMLElement(event.target)?.closest('[data-account-service-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') || '';
|
||||
|
||||
switch (action) {
|
||||
case 'open-service-panel':
|
||||
event.preventDefault();
|
||||
toggleServicePanel(panelID);
|
||||
focusElement(focusID);
|
||||
return;
|
||||
case 'manage-inline-request':
|
||||
event.preventDefault();
|
||||
requestVerificationCode('manage');
|
||||
return;
|
||||
case 'manage-inline-resend':
|
||||
resendVerificationCode('manage', event);
|
||||
return;
|
||||
case 'manage-inline-confirm':
|
||||
event.preventDefault();
|
||||
confirmVerificationCode('manage');
|
||||
return;
|
||||
case 'retrieve-inline-request':
|
||||
event.preventDefault();
|
||||
requestVerificationCode('retrieve');
|
||||
return;
|
||||
case 'retrieve-inline-confirm':
|
||||
event.preventDefault();
|
||||
confirmVerificationCode('retrieve');
|
||||
return;
|
||||
case 'retrieve-inline-copy':
|
||||
event.preventDefault();
|
||||
copyRetrievedLicense();
|
||||
return;
|
||||
case 'refund-inline-submit':
|
||||
event.preventDefault();
|
||||
submitRefund();
|
||||
return;
|
||||
case 'data-export-request':
|
||||
event.preventDefault();
|
||||
requestVerificationCode('export');
|
||||
return;
|
||||
case 'data-export-resend':
|
||||
resendVerificationCode('export', event);
|
||||
return;
|
||||
case 'data-export-confirm':
|
||||
event.preventDefault();
|
||||
confirmVerificationCode('export');
|
||||
return;
|
||||
case 'data-delete-request':
|
||||
event.preventDefault();
|
||||
requestVerificationCode('delete');
|
||||
return;
|
||||
case 'data-delete-resend':
|
||||
resendVerificationCode('delete', event);
|
||||
return;
|
||||
case 'data-delete-confirm':
|
||||
event.preventDefault();
|
||||
confirmVerificationCode('delete');
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('input', function(event) {
|
||||
var target = asHTMLElement(event.target) as FormValueElement | null;
|
||||
if (!target) return;
|
||||
var inputKind = target.getAttribute('data-account-service-input') || '';
|
||||
function updateInputValue(inputKind: string, value: string) {
|
||||
switch (inputKind) {
|
||||
case 'manage-email':
|
||||
serviceState.flows.manage.emailValue = target.value;
|
||||
serviceState.flows.manage.emailValue = value;
|
||||
return;
|
||||
case 'manage-code':
|
||||
serviceState.flows.manage.codeValue = target.value;
|
||||
serviceState.flows.manage.codeValue = value;
|
||||
return;
|
||||
case 'retrieve-email':
|
||||
serviceState.flows.retrieve.emailValue = target.value;
|
||||
serviceState.flows.retrieve.emailValue = value;
|
||||
return;
|
||||
case 'retrieve-code':
|
||||
serviceState.flows.retrieve.codeValue = target.value;
|
||||
serviceState.flows.retrieve.codeValue = value;
|
||||
return;
|
||||
case 'refund-email':
|
||||
serviceState.refund.emailValue = target.value;
|
||||
serviceState.refund.emailValue = value;
|
||||
return;
|
||||
case 'refund-token':
|
||||
serviceState.refund.tokenValue = target.value;
|
||||
serviceState.refund.tokenValue = value;
|
||||
return;
|
||||
case 'data-export-email':
|
||||
serviceState.flows.export.emailValue = target.value;
|
||||
serviceState.flows.export.emailValue = value;
|
||||
return;
|
||||
case 'data-export-code':
|
||||
serviceState.flows.export.codeValue = target.value;
|
||||
serviceState.flows.export.codeValue = value;
|
||||
return;
|
||||
case 'data-delete-email':
|
||||
serviceState.flows.delete.emailValue = target.value;
|
||||
serviceState.flows.delete.emailValue = value;
|
||||
return;
|
||||
case 'data-delete-code':
|
||||
serviceState.flows.delete.codeValue = target.value;
|
||||
serviceState.flows.delete.codeValue = value;
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
var target = asHTMLElement(event.target) as HTMLInputElement | null;
|
||||
if (!target || target.id !== 'data-delete-confirm-check') return;
|
||||
serviceState.flows.delete.checkboxChecked = !!target.checked;
|
||||
installServicesController({
|
||||
toggleServicePanel,
|
||||
focusElement,
|
||||
requestVerificationCode: function(flowID) {
|
||||
void requestVerificationCode(flowID);
|
||||
},
|
||||
resendVerificationCode: function(flowID, event) {
|
||||
void resendVerificationCode(flowID, event);
|
||||
},
|
||||
confirmVerificationCode: function(flowID) {
|
||||
void confirmVerificationCode(flowID);
|
||||
},
|
||||
copyRetrievedLicense: function() {
|
||||
void copyRetrievedLicense();
|
||||
},
|
||||
submitRefund: function() {
|
||||
void submitRefund();
|
||||
},
|
||||
updateInputValue,
|
||||
updateDeleteConfirmation: function(checked) {
|
||||
serviceState.flows.delete.checkboxChecked = checked;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
96
internal/cloudcp/portal/frontend/src/services_controller.ts
Normal file
96
internal/cloudcp/portal/frontend/src/services_controller.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { asHTMLElement } from './services_view';
|
||||
|
||||
export interface ServicesControllerDeps {
|
||||
toggleServicePanel: (panelID: string) => void;
|
||||
focusElement: (id: string) => void;
|
||||
requestVerificationCode: (flowID: 'manage' | 'retrieve' | 'export' | 'delete') => void;
|
||||
resendVerificationCode: (flowID: 'manage' | 'export' | 'delete', event?: Event) => void;
|
||||
confirmVerificationCode: (flowID: 'manage' | 'retrieve' | 'export' | 'delete') => void;
|
||||
copyRetrievedLicense: () => void;
|
||||
submitRefund: () => void;
|
||||
updateInputValue: (inputKind: string, value: string) => void;
|
||||
updateDeleteConfirmation: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export function installServicesController(deps: ServicesControllerDeps): void {
|
||||
document.addEventListener('click', function(event) {
|
||||
var target = asHTMLElement(event.target)?.closest('[data-account-service-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') || '';
|
||||
|
||||
switch (action) {
|
||||
case 'open-service-panel':
|
||||
event.preventDefault();
|
||||
deps.toggleServicePanel(panelID);
|
||||
deps.focusElement(focusID);
|
||||
return;
|
||||
case 'manage-inline-request':
|
||||
event.preventDefault();
|
||||
deps.requestVerificationCode('manage');
|
||||
return;
|
||||
case 'manage-inline-resend':
|
||||
deps.resendVerificationCode('manage', event);
|
||||
return;
|
||||
case 'manage-inline-confirm':
|
||||
event.preventDefault();
|
||||
deps.confirmVerificationCode('manage');
|
||||
return;
|
||||
case 'retrieve-inline-request':
|
||||
event.preventDefault();
|
||||
deps.requestVerificationCode('retrieve');
|
||||
return;
|
||||
case 'retrieve-inline-confirm':
|
||||
event.preventDefault();
|
||||
deps.confirmVerificationCode('retrieve');
|
||||
return;
|
||||
case 'retrieve-inline-copy':
|
||||
event.preventDefault();
|
||||
deps.copyRetrievedLicense();
|
||||
return;
|
||||
case 'refund-inline-submit':
|
||||
event.preventDefault();
|
||||
deps.submitRefund();
|
||||
return;
|
||||
case 'data-export-request':
|
||||
event.preventDefault();
|
||||
deps.requestVerificationCode('export');
|
||||
return;
|
||||
case 'data-export-resend':
|
||||
deps.resendVerificationCode('export', event);
|
||||
return;
|
||||
case 'data-export-confirm':
|
||||
event.preventDefault();
|
||||
deps.confirmVerificationCode('export');
|
||||
return;
|
||||
case 'data-delete-request':
|
||||
event.preventDefault();
|
||||
deps.requestVerificationCode('delete');
|
||||
return;
|
||||
case 'data-delete-resend':
|
||||
deps.resendVerificationCode('delete', event);
|
||||
return;
|
||||
case 'data-delete-confirm':
|
||||
event.preventDefault();
|
||||
deps.confirmVerificationCode('delete');
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
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') || '';
|
||||
if (!inputKind) return;
|
||||
deps.updateInputValue(inputKind, target.value);
|
||||
});
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
var target = asHTMLElement(event.target) as HTMLInputElement | null;
|
||||
if (!target || target.id !== 'data-delete-confirm-check') return;
|
||||
deps.updateDeleteConfirmation(!!target.checked);
|
||||
});
|
||||
}
|
||||
249
internal/cloudcp/portal/frontend/src/services_view.ts
Normal file
249
internal/cloudcp/portal/frontend/src/services_view.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
import type { PortalBootstrapData, RefundState, ServiceStatus, VerificationFlowState } from './types';
|
||||
|
||||
type FormValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
|
||||
export function getElement<T extends HTMLElement = HTMLElement>(id: string): T | null {
|
||||
return document.getElementById(id) as T | null;
|
||||
}
|
||||
|
||||
export function asHTMLElement(target: EventTarget | null): HTMLElement | null {
|
||||
return target instanceof HTMLElement ? target : null;
|
||||
}
|
||||
|
||||
export function escapeText(value: unknown): string {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
export function escapeAttribute(value: unknown): string {
|
||||
return escapeText(value)
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export function readValue(id: string): string {
|
||||
var el = getElement<FormValueElement>(id);
|
||||
return el ? el.value.trim() : '';
|
||||
}
|
||||
|
||||
export function focusElement(id: string): void {
|
||||
var el = getElement<FormValueElement>(id);
|
||||
if (el) el.focus();
|
||||
}
|
||||
|
||||
export function setVisible(id: string, visible: boolean): void {
|
||||
var el = getElement(id);
|
||||
if (el) {
|
||||
el.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export function setValue(id: string, value: string): void {
|
||||
var el = getElement<FormValueElement>(id);
|
||||
if (el) {
|
||||
el.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderStatus(id: string, status: ServiceStatus): void {
|
||||
var el = getElement(id);
|
||||
if (!el) return;
|
||||
if (!status.visible) {
|
||||
el.textContent = '';
|
||||
el.className = 'service-status';
|
||||
return;
|
||||
}
|
||||
el.textContent = status.message;
|
||||
el.className = 'service-status visible' + (status.error ? ' error' : ' success');
|
||||
}
|
||||
|
||||
export function renderButton(id: string | undefined, disabled: boolean, label: string | undefined): void {
|
||||
if (!id || !label) return;
|
||||
var button = getElement<HTMLButtonElement>(id);
|
||||
if (!button) return;
|
||||
button.disabled = disabled;
|
||||
button.textContent = label;
|
||||
}
|
||||
|
||||
export function renderOpenPanels(openPanelID: string): void {
|
||||
var panels = ['manage-service-panel', 'retrieve-service-panel', 'refund-service-panel', 'data-service-panel'];
|
||||
for (var i = 0; i < panels.length; i++) {
|
||||
var panel = getElement(panels[i]);
|
||||
if (!panel) continue;
|
||||
panel.classList.toggle('visible', panels[i] === openPanelID);
|
||||
}
|
||||
}
|
||||
|
||||
export function renderRefundPanel(refundState: RefundState, bootstrap: PortalBootstrapData): void {
|
||||
var root = getElement('refund-service-root');
|
||||
if (!root) return;
|
||||
var refundSupportURL = (bootstrap.public_site_url || '') + '/refund.html?email=' + encodeURIComponent(refundState.emailValue || '');
|
||||
root.innerHTML = '' +
|
||||
'<h3>Refund requests</h3>' +
|
||||
'<p>Process an eligible self-serve refund for a self-hosted purchase. This revokes the associated license immediately.</p>' +
|
||||
'<div class="warning"><strong>Warning:</strong> completing a refund immediately revokes the affected license. This should only be used when the refund window and commercial contract allow it.</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="refund-inline-email">Email address</label>' +
|
||||
'<input type="email" id="refund-inline-email" value="' + escapeAttribute(refundState.emailValue || '') + '" autocomplete="email" data-account-service-input="refund-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="refund-inline-token">License key</label>' +
|
||||
'<input type="text" id="refund-inline-token" value="' + escapeAttribute(refundState.tokenValue || '') + '" placeholder="pulse_xxxxx" data-account-service-input="refund-token">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-danger" type="button" id="refund-inline-submit" data-account-service-action="refund-inline-submit">Process Refund</button>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">If this purchase is not eligible for self-serve refund, use the public support path instead: <a href="' + escapeAttribute(refundSupportURL) + '">open refund support page</a>.</div>' +
|
||||
'<div class="service-status" id="refund-inline-status"></div>';
|
||||
}
|
||||
|
||||
export function renderManagePanel(flowState: VerificationFlowState): void {
|
||||
var root = getElement('manage-service-root');
|
||||
if (!root) return;
|
||||
root.innerHTML = '' +
|
||||
'<h3>Manage subscriptions</h3>' +
|
||||
'<p>Request a verification code for the commercial email, then open the Stripe customer portal for billing changes, invoices, and subscription actions.</p>' +
|
||||
'<div id="manage-inline-step1">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="manage-inline-email">Email address</label>' +
|
||||
'<input type="email" id="manage-inline-email" value="' + escapeAttribute(flowState.emailValue || '') + '" autocomplete="email" data-account-service-input="manage-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="manage-inline-request" data-account-service-action="manage-inline-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="manage-inline-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="manage-inline-code">Verification code</label>' +
|
||||
'<input type="text" id="manage-inline-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="manage-code">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="manage-inline-confirm" data-account-service-action="manage-inline-confirm">Open Customer Portal</button>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Need a new code? <a href="#" id="manage-inline-resend" data-account-service-action="manage-inline-resend">Send again</a></div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="manage-inline-status"></div>';
|
||||
}
|
||||
|
||||
export function renderRetrievePanel(flowState: VerificationFlowState): void {
|
||||
var root = getElement('retrieve-service-root');
|
||||
if (!root) return;
|
||||
var result = flowState.result as {
|
||||
invoice_url?: string;
|
||||
token?: string;
|
||||
tier?: string;
|
||||
issued_at?: string;
|
||||
expires_at?: string | null;
|
||||
email?: string;
|
||||
} | null;
|
||||
var invoiceURL = result && result.invoice_url ? result.invoice_url : '#';
|
||||
var invoiceDisplay = result && result.invoice_url ? 'inline-block' : 'none';
|
||||
var copyDisplay = result ? 'inline-block' : 'none';
|
||||
var resultDisplay = result ? 'block' : 'none';
|
||||
root.innerHTML = '' +
|
||||
'<h3>Retrieve licenses</h3>' +
|
||||
'<p>Request a verification code for the commercial email, then reveal the current active self-hosted license without leaving Pulse Account.</p>' +
|
||||
'<div id="retrieve-inline-step1">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="retrieve-inline-email">Email address</label>' +
|
||||
'<input type="email" id="retrieve-inline-email" value="' + escapeAttribute(flowState.emailValue || '') + '" autocomplete="email" data-account-service-input="retrieve-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="retrieve-inline-request" data-account-service-action="retrieve-inline-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="retrieve-inline-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="retrieve-inline-code">Verification code</label>' +
|
||||
'<input type="text" id="retrieve-inline-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="retrieve-code">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="retrieve-inline-confirm" data-account-service-action="retrieve-inline-confirm">Show License</button>' +
|
||||
'<button class="btn-secondary" type="button" id="retrieve-inline-copy" data-account-service-action="retrieve-inline-copy" style="display:' + copyDisplay + '">Copy License Key</button>' +
|
||||
'<a class="btn-secondary" id="retrieve-inline-invoice" href="' + escapeAttribute(invoiceURL) + '" target="_blank" rel="noopener" style="display:' + invoiceDisplay + '">View Invoice</a>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Use the latest active self-hosted license for this commercial email.</div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="retrieve-inline-status"></div>' +
|
||||
'<div id="retrieve-inline-result" style="display:' + resultDisplay + '; margin-top:14px">' +
|
||||
'<label for="retrieve-inline-token">License key</label>' +
|
||||
'<textarea id="retrieve-inline-token" readonly>' + escapeText(result ? result.token : '') + '</textarea>' +
|
||||
'<div class="result-grid">' +
|
||||
'<div><div class="result-meta-label">Plan</div><div class="result-meta-value" id="retrieve-inline-tier">' + escapeText(result ? result.tier : '') + '</div></div>' +
|
||||
'<div><div class="result-meta-label">Issued</div><div class="result-meta-value" id="retrieve-inline-issued">' + escapeText(result ? new Date(result.issued_at).toLocaleString() : '') + '</div></div>' +
|
||||
'<div><div class="result-meta-label">Expires</div><div class="result-meta-value" id="retrieve-inline-expires">' + escapeText(result ? (result.expires_at ? new Date(result.expires_at).toLocaleString() : 'Does not expire') : '') + '</div></div>' +
|
||||
'<div><div class="result-meta-label">Purchase Email</div><div class="result-meta-value" id="retrieve-inline-email-value">' + escapeText(result ? result.email : '') + '</div></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
export function renderExportPanel(flowState: VerificationFlowState): void {
|
||||
var root = getElement('data-export-root');
|
||||
if (!root) return;
|
||||
var resultDisplay = flowState.result ? 'block' : 'none';
|
||||
root.innerHTML = '' +
|
||||
'<h4>Export My Data</h4>' +
|
||||
'<div id="data-export-step1">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-export-email">Email address</label>' +
|
||||
'<input type="email" id="data-export-email" value="' + escapeAttribute(flowState.emailValue || '') + '" autocomplete="email" data-account-service-input="data-export-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="data-export-request" data-account-service-action="data-export-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="data-export-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-export-code">Verification code</label>' +
|
||||
'<input type="text" id="data-export-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-export-code">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="data-export-confirm" data-account-service-action="data-export-confirm">Export My Data</button>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Need a new code? <a href="#" id="data-export-resend" data-account-service-action="data-export-resend">Send again</a></div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="data-export-status"></div>' +
|
||||
'<div id="data-export-result" style="display:' + resultDisplay + '; margin-top:14px">' +
|
||||
'<label for="data-export-payload">Export payload</label>' +
|
||||
'<textarea id="data-export-payload" readonly>' + escapeText(flowState.result ? JSON.stringify(flowState.result, null, 2) : '') + '</textarea>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
export function renderExportResult(result: unknown): void {
|
||||
setVisible('data-export-result', !!result);
|
||||
setValue('data-export-payload', result ? JSON.stringify(result, null, 2) : '');
|
||||
}
|
||||
|
||||
export function renderDeletePanel(flowState: VerificationFlowState): void {
|
||||
var root = getElement('data-delete-root');
|
||||
if (!root) return;
|
||||
root.innerHTML = '' +
|
||||
'<h4>Delete My Data</h4>' +
|
||||
'<div class="warning"><strong>Warning:</strong> deleting commercial data also revokes license records and cannot be undone.</div>' +
|
||||
'<div id="data-delete-step1">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-delete-email">Email address</label>' +
|
||||
'<input type="email" id="data-delete-email" value="' + escapeAttribute(flowState.emailValue || '') + '" autocomplete="email" data-account-service-input="data-delete-email">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-danger" type="button" id="data-delete-request" data-account-service-action="data-delete-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="data-delete-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-delete-code">Verification code</label>' +
|
||||
'<input type="text" id="data-delete-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-delete-code">' +
|
||||
'</div>' +
|
||||
'<div class="checkbox-row">' +
|
||||
'<input type="checkbox" id="data-delete-confirm-check"' + (flowState.checkboxChecked ? ' checked' : '') + '>' +
|
||||
'<span>I understand this permanently deletes my commercial data and revokes associated licenses.</span>' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-danger" type="button" id="data-delete-confirm" data-account-service-action="data-delete-confirm">Delete My Data</button>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Need a new code? <a href="#" id="data-delete-resend" data-account-service-action="data-delete-resend">Send again</a></div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="data-delete-status"></div>';
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue