Return to Pulse Pro billing to refresh the upgraded entitlement. If automatic activation is unavailable, Pulse Account can still retrieve the latest active license.
' + (featureKey === "max_monitored_systems" ? "Pulse Account compares self-hosted tiers and sends completed monitored-system upgrades straight back to Pulse Pro billing." : "Pulse Account compares self-hosted tiers and sends completed checkout straight back to Pulse Pro billing.") + "
";
}
function renderButton(id, disabled, label) {
if (!id || !label) return;
@@ -1692,12 +1686,13 @@
var nextState = createPortalBillingState();
billingState.openBillingPanelID = nextState.openBillingPanelID;
billingState.upgradeFeatureKey = nextState.upgradeFeatureKey;
- billingState.upgradeReturnURL = nextState.upgradeReturnURL;
- billingState.upgradeCheckoutSessionID = nextState.upgradeCheckoutSessionID;
+ billingState.upgradeInstanceOrigin = nextState.upgradeInstanceOrigin;
+ billingState.upgradePurchaseReturnToken = nextState.upgradePurchaseReturnToken;
+ billingState.upgradeActivationURLTemplate = nextState.upgradeActivationURLTemplate;
+ billingState.upgradeHandoff = nextState.upgradeHandoff;
billingState.upgradeCheckoutStatus = nextState.upgradeCheckoutStatus;
billingState.upgradePricing = nextState.upgradePricing;
billingState.upgradeCheckout = nextState.upgradeCheckout;
- billingState.upgradeCheckoutResult = nextState.upgradeCheckoutResult;
billingState.flows = nextState.flows;
billingState.refund = nextState.refund;
}
@@ -1772,23 +1767,17 @@
return String(store.getBootstrap().portal_path || "/portal");
}
}
- function buildUpgradeCheckoutReturnURL(status) {
+ function buildUpgradeCheckoutCancelURL() {
var url = new URL(currentPortalBaseURL());
var billingState = getBillingState();
if (billingState.flows.manage.emailValue) {
url.searchParams.set("email", billingState.flows.manage.emailValue);
}
- if (billingState.upgradeFeatureKey) {
- url.searchParams.set("feature", billingState.upgradeFeatureKey);
- }
- if (billingState.upgradeReturnURL) {
- url.searchParams.set("return_url", billingState.upgradeReturnURL);
+ if (billingState.upgradePurchaseReturnToken) {
+ url.searchParams.set("purchase_return_token", billingState.upgradePurchaseReturnToken);
}
url.searchParams.set("service", "upgrade");
- url.searchParams.set("checkout", status);
- if (status === "success") {
- url.searchParams.set("session_id", "{CHECKOUT_SESSION_ID}");
- }
+ url.searchParams.set("checkout", "cancelled");
return url.toString();
}
async function loadUpgradePricing(force) {
@@ -1814,39 +1803,78 @@
});
}
}
- async function resolveCompletedCheckout(force) {
+ async function resolveUpgradeHandoff(force) {
var billingState = getBillingState();
- var sessionID = billingState.upgradeCheckoutSessionID;
- if (!sessionID) return;
- if (!force && (billingState.upgradeCheckoutResult.status === "loading" || billingState.upgradeCheckoutResult.status === "ready")) {
+ var returnToken = billingState.upgradePurchaseReturnToken;
+ if (!returnToken) return;
+ if (!force && (billingState.upgradeHandoff.status === "loading" || billingState.upgradeHandoff.status === "ready")) {
+ return;
+ }
+ if (!billingState.upgradeInstanceOrigin) {
+ updateBillingState(function(nextBillingState) {
+ failQueryState(
+ nextBillingState.upgradeHandoff,
+ null,
+ "Reopen this upgrade from Pulse Pro billing so Pulse Account can verify the return path."
+ );
+ nextBillingState.upgradeActivationURLTemplate = "";
+ });
return;
}
updateBillingState(function(nextBillingState) {
- beginQueryState(nextBillingState.upgradeCheckoutResult, null);
+ beginQueryState(nextBillingState.upgradeHandoff, null);
});
try {
- var result = await api.getCommercialJSON(
- "/v1/checkout/session?session_id=" + encodeURIComponent(sessionID)
- );
+ var handoffURL = new URL("/auth/license-purchase-handoff", billingState.upgradeInstanceOrigin);
+ handoffURL.searchParams.set("purchase_return_token", returnToken);
+ var response = await fetch(handoffURL.toString(), {
+ headers: { Accept: "application/json" }
+ });
+ if (!response.ok) {
+ var errorText = "";
+ try {
+ errorText = (await response.text()).trim();
+ } catch {
+ errorText = "";
+ }
+ throw new Error(errorText || "Failed to verify the Pulse Pro upgrade return path.");
+ }
+ var result = await response.json();
+ var resolvedFeature = String(result.feature || "").trim();
+ var activationURLTemplate = String(result.activation_url_template || "").trim();
+ if (!activationURLTemplate) {
+ throw new Error("Pulse Account could not verify the Pulse Pro checkout return path.");
+ }
updateBillingState(function(nextBillingState) {
- resolveQueryState(nextBillingState.upgradeCheckoutResult, result);
+ resolveQueryState(nextBillingState.upgradeHandoff, result);
+ nextBillingState.upgradeFeatureKey = resolvedFeature;
+ nextBillingState.upgradeActivationURLTemplate = activationURLTemplate;
});
} catch (err) {
updateBillingState(function(nextBillingState) {
failQueryState(
- nextBillingState.upgradeCheckoutResult,
+ nextBillingState.upgradeHandoff,
null,
- err instanceof Error ? err.message : "Failed to confirm the completed checkout."
+ err instanceof Error ? err.message : "Failed to verify the Pulse Pro upgrade return path."
);
+ nextBillingState.upgradeActivationURLTemplate = "";
});
}
}
async function startUpgradeCheckout(planKey, tier, billingCycle) {
if (!planKey || !tier || !billingCycle) return;
+ var activationURLTemplate = String(getBillingState().upgradeActivationURLTemplate || "").trim();
+ if (!activationURLTemplate) {
+ updateBillingState(function(nextBillingState) {
+ failMutationState(
+ nextBillingState.upgradeCheckout,
+ "Pulse Account could not verify the secure return path. Reopen the upgrade flow from Pulse Pro billing."
+ );
+ });
+ return;
+ }
updateBillingState(function(nextBillingState) {
beginMutationState(nextBillingState.upgradeCheckout);
- nextBillingState.upgradeCheckoutResult = createPortalBillingState().upgradeCheckoutResult;
- nextBillingState.upgradeCheckoutSessionID = "";
nextBillingState.upgradeCheckoutStatus = "";
});
try {
@@ -1854,8 +1882,8 @@
plan_key: planKey,
tier,
billing_cycle: billingCycle,
- success_url: buildUpgradeCheckoutReturnURL("success"),
- cancel_url: buildUpgradeCheckoutReturnURL("cancelled")
+ success_url: activationURLTemplate,
+ cancel_url: buildUpgradeCheckoutCancelURL()
});
if (!data || !data.url) {
throw new Error("Checkout URL was not returned.");
@@ -2153,11 +2181,9 @@
}
function renderBillingRuntime() {
var billingState = getBillingState();
- if (billingState.openBillingPanelID === "upgrade-billing-panel" || !!billingState.upgradeFeatureKey || billingState.upgradeCheckoutStatus === "success") {
+ if (billingState.openBillingPanelID === "upgrade-billing-panel" || !!billingState.upgradeFeatureKey || !!billingState.upgradePurchaseReturnToken) {
void loadUpgradePricing(false);
- }
- if (billingState.upgradeCheckoutStatus === "success" && billingState.upgradeCheckoutSessionID) {
- void resolveCompletedCheckout(false);
+ void resolveUpgradeHandoff(false);
}
renderOpenBillingPanels(getBillingState().openBillingPanelID);
renderAllFlows();
@@ -2278,7 +2304,7 @@
}
function renderSelfHostedUpgradeBillingPanel(context) {
var featureKey = normalizeUpgradeFeatureKey(context.billingState.upgradeFeatureKey);
- var helperCopy = featureKey === "max_monitored_systems" ? "Choose the self-hosted tier that matches the monitored-system allowance you need, then send the completed purchase back to Pulse Pro for activation." : "Choose the self-hosted tier that fits this upgrade, then send the completed purchase back to Pulse Pro for activation.";
+ var helperCopy = featureKey === "max_monitored_systems" ? "Choose the self-hosted tier that matches the monitored-system allowance you need. Pulse Account will send completed checkout directly back to Pulse Pro billing." : "Choose the self-hosted tier that fits this upgrade. Pulse Account will send completed checkout directly back to Pulse Pro billing.";
return renderBillingTaskPanel(
selfHostedUpgradeActionTitle(featureKey),
"Pulse Account owns self-hosted plan selection and checkout for Pulse Pro upgrades.",
@@ -2936,6 +2962,15 @@
function normalizeHandoffEmail(value) {
return String(value || "").trim();
}
+ function normalizeHandoffInstanceOrigin(value) {
+ var trimmed = String(value || "").trim();
+ if (!trimmed) return "";
+ try {
+ return new URL(trimmed).origin;
+ } catch {
+ return "";
+ }
+ }
function normalizeHandoffBillingPanel(value) {
switch (String(value || "").trim()) {
case "upgrade":
@@ -2955,10 +2990,7 @@
function normalizeUpgradeFeatureKey2(value) {
return String(value || "").trim();
}
- function normalizeUpgradeReturnURL(value) {
- return String(value || "").trim();
- }
- function normalizeUpgradeCheckoutSessionID(value) {
+ function normalizeUpgradePurchaseReturnToken(value) {
return String(value || "").trim();
}
function normalizeUpgradeCheckoutStatus(value) {
@@ -2971,24 +3003,24 @@
return "";
}
}
- function readPortalRuntimeHandoff(locationHref = window.location.href) {
+ function readPortalRuntimeHandoff(locationHref = window.location.href, referrerHref = typeof document !== "undefined" ? document.referrer : "") {
try {
var params = new URL(locationHref).searchParams;
return {
email: normalizeHandoffEmail(params.get("email")),
openBillingPanelID: normalizeHandoffBillingPanel(params.get("service")),
- upgradeFeatureKey: normalizeUpgradeFeatureKey2(params.get("feature")),
- upgradeReturnURL: normalizeUpgradeReturnURL(params.get("return_url")),
- upgradeCheckoutSessionID: normalizeUpgradeCheckoutSessionID(params.get("session_id")),
+ upgradeInstanceOrigin: normalizeHandoffInstanceOrigin(referrerHref),
+ upgradeFeatureKey: normalizeUpgradeFeatureKey2(params.get("purchase_return_token") ? "" : params.get("feature")),
+ upgradePurchaseReturnToken: normalizeUpgradePurchaseReturnToken(params.get("purchase_return_token")),
upgradeCheckoutStatus: normalizeUpgradeCheckoutStatus(params.get("checkout"))
};
} catch {
return {
email: "",
openBillingPanelID: "",
+ upgradeInstanceOrigin: "",
upgradeFeatureKey: "",
- upgradeReturnURL: "",
- upgradeCheckoutSessionID: "",
+ upgradePurchaseReturnToken: "",
upgradeCheckoutStatus: ""
};
}
@@ -3028,17 +3060,19 @@
store.setActiveShellSection("billing");
store.updateBillingState(function(billingState) {
billingState.openBillingPanelID = handoff.openBillingPanelID;
+ billingState.upgradeInstanceOrigin = handoff.upgradeInstanceOrigin;
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
- billingState.upgradeReturnURL = handoff.upgradeReturnURL;
- billingState.upgradeCheckoutSessionID = handoff.upgradeCheckoutSessionID;
+ billingState.upgradePurchaseReturnToken = handoff.upgradePurchaseReturnToken;
+ billingState.upgradeActivationURLTemplate = "";
billingState.upgradeCheckoutStatus = handoff.upgradeCheckoutStatus;
}, { notify: false });
- } else if (handoff.upgradeFeatureKey) {
+ } else if (handoff.upgradeFeatureKey || handoff.upgradePurchaseReturnToken) {
store.setActiveShellSection("billing");
store.updateBillingState(function(billingState) {
+ billingState.upgradeInstanceOrigin = handoff.upgradeInstanceOrigin;
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
- billingState.upgradeReturnURL = handoff.upgradeReturnURL;
- billingState.upgradeCheckoutSessionID = handoff.upgradeCheckoutSessionID;
+ billingState.upgradePurchaseReturnToken = handoff.upgradePurchaseReturnToken;
+ billingState.upgradeActivationURLTemplate = "";
billingState.upgradeCheckoutStatus = handoff.upgradeCheckoutStatus;
}, { notify: false });
}
diff --git a/internal/cloudcp/portal/frontend/src/app.test.ts b/internal/cloudcp/portal/frontend/src/app.test.ts
index f3b5f3911..e8bdeac28 100644
--- a/internal/cloudcp/portal/frontend/src/app.test.ts
+++ b/internal/cloudcp/portal/frontend/src/app.test.ts
@@ -150,10 +150,9 @@ describe('portal app', function() {
{
email: 'buyer@example.com',
openBillingPanelID: 'retrieve-billing-panel',
+ upgradeInstanceOrigin: '',
upgradeFeatureKey: '',
- upgradeReturnURL: '',
upgradePurchaseReturnToken: '',
- upgradeCheckoutSessionID: '',
upgradeCheckoutStatus: '',
}
);
diff --git a/internal/cloudcp/portal/frontend/src/billing.ts b/internal/cloudcp/portal/frontend/src/billing.ts
index 14e40df83..21157e56b 100644
--- a/internal/cloudcp/portal/frontend/src/billing.ts
+++ b/internal/cloudcp/portal/frontend/src/billing.ts
@@ -41,7 +41,7 @@ import type {
PortalBillingFlowID,
PortalBillingState,
PortalCheckoutSessionCreateResponse,
- PortalCheckoutSessionResult,
+ PortalUpgradeHandoffModel,
PortalUpgradePricingModel,
VerificationFlowState,
} from './types';
@@ -88,13 +88,13 @@ export function installBillingRuntime(deps: BillingRuntimeDeps): void {
var nextState = createPortalBillingState();
billingState.openBillingPanelID = nextState.openBillingPanelID;
billingState.upgradeFeatureKey = nextState.upgradeFeatureKey;
- billingState.upgradeReturnURL = nextState.upgradeReturnURL;
+ billingState.upgradeInstanceOrigin = nextState.upgradeInstanceOrigin;
billingState.upgradePurchaseReturnToken = nextState.upgradePurchaseReturnToken;
- billingState.upgradeCheckoutSessionID = nextState.upgradeCheckoutSessionID;
+ billingState.upgradeActivationURLTemplate = nextState.upgradeActivationURLTemplate;
+ billingState.upgradeHandoff = nextState.upgradeHandoff;
billingState.upgradeCheckoutStatus = nextState.upgradeCheckoutStatus;
billingState.upgradePricing = nextState.upgradePricing;
billingState.upgradeCheckout = nextState.upgradeCheckout;
- billingState.upgradeCheckoutResult = nextState.upgradeCheckoutResult;
billingState.flows = nextState.flows;
billingState.refund = nextState.refund;
}
@@ -180,26 +180,17 @@ export function installBillingRuntime(deps: BillingRuntimeDeps): void {
}
}
- function buildUpgradeCheckoutReturnURL(status: 'success' | 'cancelled'): string {
+ function buildUpgradeCheckoutCancelURL(): string {
var url = new URL(currentPortalBaseURL());
var billingState = getBillingState();
if (billingState.flows.manage.emailValue) {
url.searchParams.set('email', billingState.flows.manage.emailValue);
}
- if (billingState.upgradeFeatureKey) {
- url.searchParams.set('feature', billingState.upgradeFeatureKey);
- }
- if (billingState.upgradeReturnURL) {
- url.searchParams.set('return_url', billingState.upgradeReturnURL);
- }
if (billingState.upgradePurchaseReturnToken) {
url.searchParams.set('purchase_return_token', billingState.upgradePurchaseReturnToken);
}
url.searchParams.set('service', 'upgrade');
- url.searchParams.set('checkout', status);
- if (status === 'success') {
- url.searchParams.set('session_id', '{CHECKOUT_SESSION_ID}');
- }
+ url.searchParams.set('checkout', 'cancelled');
return url.toString();
}
@@ -227,40 +218,79 @@ export function installBillingRuntime(deps: BillingRuntimeDeps): void {
}
}
- async function resolveCompletedCheckout(force: boolean) {
+ async function resolveUpgradeHandoff(force: boolean) {
var billingState = getBillingState();
- var sessionID = billingState.upgradeCheckoutSessionID;
- if (!sessionID) return;
- if (!force && (billingState.upgradeCheckoutResult.status === 'loading' || billingState.upgradeCheckoutResult.status === 'ready')) {
+ var returnToken = billingState.upgradePurchaseReturnToken;
+ if (!returnToken) return;
+ if (!force && (billingState.upgradeHandoff.status === 'loading' || billingState.upgradeHandoff.status === 'ready')) {
+ return;
+ }
+ if (!billingState.upgradeInstanceOrigin) {
+ updateBillingState(function(nextBillingState) {
+ failQueryState(
+ nextBillingState.upgradeHandoff,
+ null,
+ 'Reopen this upgrade from Pulse Pro billing so Pulse Account can verify the return path.',
+ );
+ nextBillingState.upgradeActivationURLTemplate = '';
+ });
return;
}
updateBillingState(function(nextBillingState) {
- beginQueryState(nextBillingState.upgradeCheckoutResult, null);
+ beginQueryState(nextBillingState.upgradeHandoff, null);
});
try {
- var result = await api.getCommercialJSON(
- '/v1/checkout/session?session_id=' + encodeURIComponent(sessionID),
- );
+ var handoffURL = new URL('/auth/license-purchase-handoff', billingState.upgradeInstanceOrigin);
+ handoffURL.searchParams.set('purchase_return_token', returnToken);
+ var response = await fetch(handoffURL.toString(), {
+ headers: { Accept: 'application/json' },
+ });
+ if (!response.ok) {
+ var errorText = '';
+ try {
+ errorText = (await response.text()).trim();
+ } catch {
+ errorText = '';
+ }
+ throw new Error(errorText || 'Failed to verify the Pulse Pro upgrade return path.');
+ }
+ var result = await response.json() as PortalUpgradeHandoffModel;
+ var resolvedFeature = String(result.feature || '').trim();
+ var activationURLTemplate = String(result.activation_url_template || '').trim();
+ if (!activationURLTemplate) {
+ throw new Error('Pulse Account could not verify the Pulse Pro checkout return path.');
+ }
updateBillingState(function(nextBillingState) {
- resolveQueryState(nextBillingState.upgradeCheckoutResult, result);
+ resolveQueryState(nextBillingState.upgradeHandoff, result);
+ nextBillingState.upgradeFeatureKey = resolvedFeature;
+ nextBillingState.upgradeActivationURLTemplate = activationURLTemplate;
});
} catch (err) {
updateBillingState(function(nextBillingState) {
failQueryState(
- nextBillingState.upgradeCheckoutResult,
+ nextBillingState.upgradeHandoff,
null,
- err instanceof Error ? err.message : 'Failed to confirm the completed checkout.',
+ err instanceof Error ? err.message : 'Failed to verify the Pulse Pro upgrade return path.',
);
+ nextBillingState.upgradeActivationURLTemplate = '';
});
}
}
async function startUpgradeCheckout(planKey: string, tier: string, billingCycle: string) {
if (!planKey || !tier || !billingCycle) return;
+ var activationURLTemplate = String(getBillingState().upgradeActivationURLTemplate || '').trim();
+ if (!activationURLTemplate) {
+ updateBillingState(function(nextBillingState) {
+ failMutationState(
+ nextBillingState.upgradeCheckout,
+ 'Pulse Account could not verify the secure return path. Reopen the upgrade flow from Pulse Pro billing.',
+ );
+ });
+ return;
+ }
updateBillingState(function(nextBillingState) {
beginMutationState(nextBillingState.upgradeCheckout);
- nextBillingState.upgradeCheckoutResult = createPortalBillingState().upgradeCheckoutResult;
- nextBillingState.upgradeCheckoutSessionID = '';
nextBillingState.upgradeCheckoutStatus = '';
});
try {
@@ -268,8 +298,8 @@ export function installBillingRuntime(deps: BillingRuntimeDeps): void {
plan_key: planKey,
tier: tier,
billing_cycle: billingCycle,
- success_url: buildUpgradeCheckoutReturnURL('success'),
- cancel_url: buildUpgradeCheckoutReturnURL('cancelled'),
+ success_url: activationURLTemplate,
+ cancel_url: buildUpgradeCheckoutCancelURL(),
});
if (!data || !data.url) {
throw new Error('Checkout URL was not returned.');
@@ -576,12 +606,10 @@ export function installBillingRuntime(deps: BillingRuntimeDeps): void {
if (
billingState.openBillingPanelID === 'upgrade-billing-panel' ||
!!billingState.upgradeFeatureKey ||
- billingState.upgradeCheckoutStatus === 'success'
+ !!billingState.upgradePurchaseReturnToken
) {
void loadUpgradePricing(false);
- }
- if (billingState.upgradeCheckoutStatus === 'success' && billingState.upgradeCheckoutSessionID) {
- void resolveCompletedCheckout(false);
+ void resolveUpgradeHandoff(false);
}
renderOpenBillingPanels(getBillingState().openBillingPanelID);
renderAllFlows();
diff --git a/internal/cloudcp/portal/frontend/src/billing_view.test.ts b/internal/cloudcp/portal/frontend/src/billing_view.test.ts
index 269c77437..8f5deb328 100644
--- a/internal/cloudcp/portal/frontend/src/billing_view.test.ts
+++ b/internal/cloudcp/portal/frontend/src/billing_view.test.ts
@@ -223,14 +223,18 @@ describe('services view', function() {
expect(document.getElementById('manage-inline-status')?.textContent).toBe('Code sent.');
});
- it('renders upgrade panel with canonical plans and a return-to-product activation form', function() {
+ it('renders upgrade panel with verified direct-return checkout actions', function() {
document.body.innerHTML = '';
var billingState = createPortalBillingState();
billingState.upgradeFeatureKey = 'max_monitored_systems';
- billingState.upgradeReturnURL = 'https://pulse.example.com/auth/license-purchase-activate';
billingState.upgradePurchaseReturnToken = 'prt_signed';
- billingState.upgradeCheckoutSessionID = 'cs_success';
+ billingState.upgradeActivationURLTemplate = 'https://pulse.example.com/auth/license-purchase-activate?purchase_return_token=prt_signed&session_id={CHECKOUT_SESSION_ID}';
+ billingState.upgradeHandoff.status = 'ready';
+ billingState.upgradeHandoff.data = {
+ feature: 'max_monitored_systems',
+ activation_url_template: billingState.upgradeActivationURLTemplate,
+ };
billingState.upgradePricing.status = 'ready';
billingState.upgradePricing.data = {
title: 'Pricing',
@@ -257,45 +261,60 @@ describe('services view', function() {
},
],
};
- billingState.upgradeCheckoutResult.status = 'ready';
- billingState.upgradeCheckoutResult.data = {
- status: 'fulfilled',
- owner_email: 'buyer@example.com',
- tier: 'pro_plus',
- activation_key_prefix: 'ppk_live_preview',
- max_monitored_systems: 50,
- };
renderUpgradePanel(billingState, createBootstrap());
expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain('Buy Annual');
- expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain('Activate in Pulse Pro');
- expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain('session_id');
expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain(
- 'purchase_return_token',
+ 'Pulse Account will return completed checkout directly to Pulse Pro billing.',
);
- expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain('ppk_live_preview');
+ expect(document.getElementById('upgrade-billing-root')?.innerHTML).not.toContain('Activate in Pulse Pro');
+ expect(document.getElementById('upgrade-billing-root')?.innerHTML).not.toContain('ppk_live_preview');
+ expect(
+ (document.querySelector('[data-account-billing-action="upgrade-start-checkout"]') as HTMLButtonElement).disabled,
+ ).toBe(false);
});
- it('renders a product-refresh fallback when a completed checkout has no return URL', function() {
+ it('renders a blocked checkout state until the Pulse Pro handoff is verified', function() {
document.body.innerHTML = '';
var billingState = createPortalBillingState();
- billingState.upgradeFeatureKey = 'max_monitored_systems';
- billingState.upgradeCheckoutSessionID = 'cs_success';
- billingState.upgradeCheckoutResult.status = 'ready';
- billingState.upgradeCheckoutResult.data = {
- status: 'fulfilled',
- owner_email: 'buyer@example.com',
- tier: 'pro_plus',
- activation_key_prefix: 'ppk_live_preview',
- max_monitored_systems: 50,
+ billingState.upgradePurchaseReturnToken = 'prt_signed';
+ billingState.upgradeHandoff.status = 'error';
+ billingState.upgradeHandoff.error = 'Pulse Account could not verify the secure return path.';
+ billingState.upgradePricing.status = 'ready';
+ billingState.upgradePricing.data = {
+ title: 'Pricing',
+ description: 'Canonical pricing model',
+ plans: [
+ {
+ tierKicker: 'Relay',
+ title: 'Relay',
+ price: '$4.99',
+ period: '$39/year available too',
+ blurb: 'Secure remote access and mobile access.',
+ features: [{ tone: 'check', html: 'Up to 8 monitored systems' }],
+ buttons: [
+ {
+ kind: 'checkout',
+ className: 'btn btn-primary',
+ tier: 'relay',
+ planKey: 'price_relay_annual',
+ billingCycle: 'annual',
+ label: 'Buy Annual',
+ },
+ ],
+ },
+ ],
};
renderUpgradePanel(billingState, createBootstrap());
expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain(
- 'Return to Pulse Pro billing and reopen the upgrade flow',
+ 'Pulse Account could not verify the secure return path.',
);
+ expect(
+ (document.querySelector('[data-account-billing-action="upgrade-start-checkout"]') as HTMLButtonElement).disabled,
+ ).toBe(true);
});
});
diff --git a/internal/cloudcp/portal/frontend/src/billing_view.ts b/internal/cloudcp/portal/frontend/src/billing_view.ts
index ef4711b42..40761f8a6 100644
--- a/internal/cloudcp/portal/frontend/src/billing_view.ts
+++ b/internal/cloudcp/portal/frontend/src/billing_view.ts
@@ -73,13 +73,6 @@ export function renderBillingStatus(id: string, status: BillingStatus): void {
el.className = 'billing-status visible' + (status.error ? ' error' : ' success');
}
-function formatCheckoutDate(value: string | undefined): string {
- if (!value) return '';
- var parsed = new Date(value);
- if (Number.isNaN(parsed.getTime())) return '';
- return parsed.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
-}
-
function renderUpgradePlansHTML(billingState: PortalBillingState): string {
var pricing = billingState.upgradePricing.data;
if (!pricing || !Array.isArray(pricing.plans)) {
@@ -95,6 +88,10 @@ function renderUpgradePlansHTML(billingState: PortalBillingState): string {
return '';
}
+ var checkoutDisabled = billingState.upgradeCheckout.pending ||
+ billingState.upgradeHandoff.status === 'loading' ||
+ !String(billingState.upgradeActivationURLTemplate || '').trim();
+
return '
Return to Pulse Pro billing and reopen the upgrade flow so Pulse can securely finalize this completed checkout. If automatic return is unavailable, Pulse Account can still retrieve the latest active license.