mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Add the License-panel Pro trial front door (pull-only)
Completes the approved trial flow end to end. The License panel's plan comparison gains a 'Start 14-day free Pro trial' action (shown only when unlicensed, trial-eligible, and never-trialed) that rides the existing purchase-start handoff with trial=1; the handler already forwards non-reserved params. Pulse Account portal reads the trial arrival flag, relabels the pro monthly checkout button, shows the card-required note, and sends trial:true to the checkout session endpoint (server support deployed in pulse-pro 4c3a28c). Organic portal and public pricing arrivals see no trial anywhere, per the no-upsell doctrine; retired local trial-acquisition routes remain 404. Portal dist rebuilt.
This commit is contained in:
parent
34f64e68fc
commit
b0792890b0
13 changed files with 229 additions and 22 deletions
|
|
@ -79,6 +79,11 @@ interface ProLicensePlanSectionProps {
|
||||||
label: string;
|
label: string;
|
||||||
destination: UpgradeDestination;
|
destination: UpgradeDestination;
|
||||||
} | null;
|
} | null;
|
||||||
|
trialAction: {
|
||||||
|
label: string;
|
||||||
|
note: string;
|
||||||
|
destination: UpgradeDestination;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
planSelectionPrompt: ActionNotice | null;
|
planSelectionPrompt: ActionNotice | null;
|
||||||
planStatus: {
|
planStatus: {
|
||||||
|
|
@ -373,17 +378,34 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.planComparisonSummary.action}>
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
{(action) => (
|
<Show when={props.planComparisonSummary.trialAction}>
|
||||||
<UpgradeButtonLink
|
{(trialAction) => (
|
||||||
variant="outline"
|
<UpgradeButtonLink
|
||||||
size="settingsActionXs"
|
variant="primary"
|
||||||
class="mt-4 gap-1"
|
size="settingsActionXs"
|
||||||
destination={action().destination}
|
class="mt-4 gap-1"
|
||||||
>
|
destination={trialAction().destination}
|
||||||
{action().label}
|
>
|
||||||
</UpgradeButtonLink>
|
{trialAction().label}
|
||||||
)}
|
</UpgradeButtonLink>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={props.planComparisonSummary.action}>
|
||||||
|
{(action) => (
|
||||||
|
<UpgradeButtonLink
|
||||||
|
variant="outline"
|
||||||
|
size="settingsActionXs"
|
||||||
|
class="mt-4 gap-1"
|
||||||
|
destination={action().destination}
|
||||||
|
>
|
||||||
|
{action().label}
|
||||||
|
</UpgradeButtonLink>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={props.planComparisonSummary.trialAction}>
|
||||||
|
{(trialAction) => <p class="mt-2 text-xs text-muted">{trialAction().note}</p>}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { describe, expect, it, afterEach } from 'vitest';
|
||||||
|
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||||
|
import { Route, Router } from '@solidjs/router';
|
||||||
|
import type { Component } from 'solid-js';
|
||||||
|
import { ProLicensePlanSection } from '../ProLicensePlanSection';
|
||||||
|
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from '../selfHostedBillingPresentation';
|
||||||
|
import { resolveUpgradeDestination } from '@/utils/upgradeNavigation';
|
||||||
|
|
||||||
|
const baseProps = () => ({
|
||||||
|
activationSuccessSummary: null,
|
||||||
|
commercialMigrationNotice: null,
|
||||||
|
commercialPlanModel: { summary: [], details: [] },
|
||||||
|
currentPlanSummary: {
|
||||||
|
title: 'Community',
|
||||||
|
body: 'Core monitoring',
|
||||||
|
badgeClass: '',
|
||||||
|
statusLabel: 'Free',
|
||||||
|
unlockedFeaturesLabel: 'Included',
|
||||||
|
unlockedFeatures: [],
|
||||||
|
includedExtras: [],
|
||||||
|
supplementalBadges: [],
|
||||||
|
},
|
||||||
|
entitlements: null,
|
||||||
|
grandfatheredPriceNotice: null,
|
||||||
|
hasLicenseDetails: false,
|
||||||
|
loading: false,
|
||||||
|
onReload: () => {},
|
||||||
|
planSelectionPrompt: null,
|
||||||
|
planStatus: null,
|
||||||
|
purchaseActivationNotice: null,
|
||||||
|
purchaseActivationAction: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderInRouter = (component: Component) => {
|
||||||
|
render(() => (
|
||||||
|
<Router>
|
||||||
|
<Route path="/" component={component} />
|
||||||
|
</Router>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ProLicensePlanSection trial action', () => {
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
|
it('renders the trial button with its card-required note and trial link', () => {
|
||||||
|
renderInRouter(() => (
|
||||||
|
<ProLicensePlanSection
|
||||||
|
{...baseProps()}
|
||||||
|
planComparisonSummary={{
|
||||||
|
cards: [{ title: 'Pro', body: 'Everything', highlights: [] }],
|
||||||
|
action: {
|
||||||
|
label: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonActionLabel,
|
||||||
|
destination: resolveUpgradeDestination(
|
||||||
|
'/auth/license-purchase-start?feature=self_hosted_plan',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
trialAction: {
|
||||||
|
label: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonTrialActionLabel,
|
||||||
|
note: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonTrialActionNote,
|
||||||
|
destination: resolveUpgradeDestination(
|
||||||
|
'/auth/license-purchase-start?trial=1&feature=self_hosted_plan',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
const trialLink = screen.getByRole('link', {
|
||||||
|
name: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonTrialActionLabel,
|
||||||
|
});
|
||||||
|
expect(trialLink.getAttribute('href')).toContain('trial=1');
|
||||||
|
expect(trialLink.getAttribute('href')).toContain('feature=self_hosted_plan');
|
||||||
|
expect(
|
||||||
|
screen.getByText(SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonTrialActionNote),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no trial button when trialAction is null', () => {
|
||||||
|
renderInRouter(() => (
|
||||||
|
<ProLicensePlanSection
|
||||||
|
{...baseProps()}
|
||||||
|
planComparisonSummary={{
|
||||||
|
cards: [{ title: 'Pro', body: 'Everything', highlights: [] }],
|
||||||
|
action: {
|
||||||
|
label: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonActionLabel,
|
||||||
|
destination: resolveUpgradeDestination(
|
||||||
|
'/auth/license-purchase-start?feature=self_hosted_plan',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
trialAction: null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText(SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonTrialActionLabel),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
screen.queryByText(SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonTrialActionNote),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -12,6 +12,8 @@ export interface SelfHostedProBillingPresentation {
|
||||||
planSectionDescription: string;
|
planSectionDescription: string;
|
||||||
planComparisonSectionTitle: string;
|
planComparisonSectionTitle: string;
|
||||||
planComparisonActionLabel: string;
|
planComparisonActionLabel: string;
|
||||||
|
planComparisonTrialActionLabel: string;
|
||||||
|
planComparisonTrialActionNote: string;
|
||||||
usageSectionTitle: string;
|
usageSectionTitle: string;
|
||||||
hiddenShellTitle: string;
|
hiddenShellTitle: string;
|
||||||
hiddenShellDescription: string;
|
hiddenShellDescription: string;
|
||||||
|
|
@ -46,6 +48,9 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
|
||||||
planSectionDescription: 'Current tier and enabled capabilities.',
|
planSectionDescription: 'Current tier and enabled capabilities.',
|
||||||
planComparisonSectionTitle: 'Available plans',
|
planComparisonSectionTitle: 'Available plans',
|
||||||
planComparisonActionLabel: 'View plans',
|
planComparisonActionLabel: 'View plans',
|
||||||
|
planComparisonTrialActionLabel: 'Start 14-day free Pro trial',
|
||||||
|
planComparisonTrialActionNote:
|
||||||
|
'Card required. You will not be charged if you cancel during the trial.',
|
||||||
usageSectionTitle: 'Usage',
|
usageSectionTitle: 'Usage',
|
||||||
hiddenShellTitle: 'Demo mode',
|
hiddenShellTitle: 'Demo mode',
|
||||||
hiddenShellDescription: 'Commercial settings are hidden for this session.',
|
hiddenShellDescription: 'Commercial settings are hidden for this session.',
|
||||||
|
|
|
||||||
|
|
@ -394,21 +394,43 @@ export function useProLicensePanelState() {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const planStatus = createMemo(() => getSelfHostedPlanStatusPresentation(entitlements()));
|
const planStatus = createMemo(() => getSelfHostedPlanStatusPresentation(entitlements()));
|
||||||
|
const trialActionEligible = createMemo(() => {
|
||||||
|
const current = entitlements();
|
||||||
|
const state = subscriptionState();
|
||||||
|
if (state === 'active' || state === 'trial') return false;
|
||||||
|
if (current?.trial_expires_at) return false;
|
||||||
|
if (current?.trial_eligible === false) return false;
|
||||||
|
const tier = current?.tier;
|
||||||
|
return !tier || tier === 'free';
|
||||||
|
});
|
||||||
|
|
||||||
const planComparisonSummary = createMemo(() => {
|
const planComparisonSummary = createMemo(() => {
|
||||||
if (!showPlanSelectionPrompt()) {
|
if (!showPlanSelectionPrompt()) {
|
||||||
return { cards: [], action: null };
|
return { cards: [], action: null, trialAction: null };
|
||||||
}
|
}
|
||||||
const comparison = getSelfHostedPlanComparisonPresentation({
|
const comparison = getSelfHostedPlanComparisonPresentation({
|
||||||
entitlements: entitlements(),
|
entitlements: entitlements(),
|
||||||
});
|
});
|
||||||
|
const showActions =
|
||||||
|
comparison.cards.length > 0 && purchaseActivationResult().trim().length === 0;
|
||||||
return {
|
return {
|
||||||
...comparison,
|
...comparison,
|
||||||
action:
|
action: showActions
|
||||||
comparison.cards.length > 0 && purchaseActivationResult().trim().length === 0
|
? {
|
||||||
|
label: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonActionLabel,
|
||||||
|
destination: resolveSelfHostedPurchaseStartDestination(
|
||||||
|
SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
trialAction:
|
||||||
|
showActions && trialActionEligible()
|
||||||
? {
|
? {
|
||||||
label: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonActionLabel,
|
label: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonTrialActionLabel,
|
||||||
|
note: SELF_HOSTED_PRO_BILLING_PRESENTATION.planComparisonTrialActionNote,
|
||||||
destination: resolveSelfHostedPurchaseStartDestination(
|
destination: resolveSelfHostedPurchaseStartDestination(
|
||||||
SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT,
|
SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT,
|
||||||
|
new URLSearchParams({ trial: '1' }),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"source_hash": "ece2119b2e5fc7be4f49d6b42ab4a214497afac1670c5cd14486678ee54b78e8",
|
"source_hash": "90e5d2ec651ba11669d98c23aa107ea7dc8e07614097c0c655b3e75ded720d17",
|
||||||
"build_inputs": [
|
"build_inputs": [
|
||||||
"package.json",
|
"package.json",
|
||||||
"tsconfig.json",
|
"tsconfig.json",
|
||||||
|
|
|
||||||
24
internal/cloudcp/portal/dist/portal_app.js
vendored
24
internal/cloudcp/portal/dist/portal_app.js
vendored
|
|
@ -1185,6 +1185,7 @@
|
||||||
return {
|
return {
|
||||||
openBillingPanelID: "",
|
openBillingPanelID: "",
|
||||||
upgradeFeatureKey: "",
|
upgradeFeatureKey: "",
|
||||||
|
upgradeTrialRequested: false,
|
||||||
upgradePortalHandoffID: "",
|
upgradePortalHandoffID: "",
|
||||||
upgradePortalHandoff: createQueryState(null),
|
upgradePortalHandoff: createQueryState(null),
|
||||||
upgradePricing: createQueryState(null),
|
upgradePricing: createQueryState(null),
|
||||||
|
|
@ -1812,8 +1813,11 @@
|
||||||
return '<article class="billing-upgrade-plan-card' + (plan.highlight ? " highlight" : "") + '">' + (plan.badge ? '<div class="billing-upgrade-plan-badge">' + escapeText(plan.badge) + "</div>" : "") + '<div class="billing-upgrade-plan-header"><div class="billing-upgrade-plan-kicker">' + escapeText(plan.tierKicker) + "</div><h4>" + escapeText(plan.title) + '</h4><div class="billing-upgrade-plan-price">' + escapeText(plan.price) + '</div><div class="billing-upgrade-plan-period">' + escapeText(plan.period) + '</div></div><p class="billing-upgrade-plan-blurb">' + escapeText(plan.blurb) + '</p><ul class="billing-upgrade-plan-features">' + plan.features.map(function(feature) {
|
return '<article class="billing-upgrade-plan-card' + (plan.highlight ? " highlight" : "") + '">' + (plan.badge ? '<div class="billing-upgrade-plan-badge">' + escapeText(plan.badge) + "</div>" : "") + '<div class="billing-upgrade-plan-header"><div class="billing-upgrade-plan-kicker">' + escapeText(plan.tierKicker) + "</div><h4>" + escapeText(plan.title) + '</h4><div class="billing-upgrade-plan-price">' + escapeText(plan.price) + '</div><div class="billing-upgrade-plan-period">' + escapeText(plan.period) + '</div></div><p class="billing-upgrade-plan-blurb">' + escapeText(plan.blurb) + '</p><ul class="billing-upgrade-plan-features">' + plan.features.map(function(feature) {
|
||||||
return '<li class="billing-upgrade-plan-feature tone-' + escapeAttribute(feature.tone) + '"><span class="billing-upgrade-plan-feature-copy">' + String(feature.html || "") + "</span></li>";
|
return '<li class="billing-upgrade-plan-feature tone-' + escapeAttribute(feature.tone) + '"><span class="billing-upgrade-plan-feature-copy">' + String(feature.html || "") + "</span></li>";
|
||||||
}).join("") + "</ul>" + (plan.note ? '<div class="helper-text">' + escapeText(plan.note) + "</div>" : "") + '<div class="form-actions">' + checkoutButtons.map(function(button) {
|
}).join("") + "</ul>" + (plan.note ? '<div class="helper-text">' + escapeText(plan.note) + "</div>" : "") + '<div class="form-actions">' + checkoutButtons.map(function(button) {
|
||||||
return '<button type="button" class="' + escapeAttribute(button.className || "btn-primary") + '" data-account-billing-action="upgrade-start-checkout" data-upgrade-plan-key="' + escapeAttribute(button.planKey || "") + '" data-upgrade-tier="' + escapeAttribute(button.tier || "") + '" data-upgrade-billing-cycle="' + escapeAttribute(button.billingCycle || "") + '"' + (checkoutDisabled ? " disabled" : "") + ">" + escapeText(button.label) + "</button>";
|
var isTrialButton = billingState.upgradeTrialRequested === true && button.tier === "pro" && button.billingCycle === "monthly";
|
||||||
}).join("") + "</div></article>";
|
return '<button type="button" class="' + escapeAttribute(button.className || "btn-primary") + '" data-account-billing-action="upgrade-start-checkout" data-upgrade-plan-key="' + escapeAttribute(button.planKey || "") + '" data-upgrade-tier="' + escapeAttribute(button.tier || "") + '" data-upgrade-billing-cycle="' + escapeAttribute(button.billingCycle || "") + '"' + (checkoutDisabled ? " disabled" : "") + ">" + escapeText(isTrialButton ? "Start 14-day free trial" : button.label) + "</button>";
|
||||||
|
}).join("") + "</div>" + (billingState.upgradeTrialRequested === true && checkoutButtons.some(function(button) {
|
||||||
|
return button.tier === "pro" && button.billingCycle === "monthly";
|
||||||
|
}) ? '<div class="helper-text">Card required. You will not be charged if you cancel during the 14-day trial.</div>' : "") + "</article>";
|
||||||
}).join("") + "</div>";
|
}).join("") + "</div>";
|
||||||
}
|
}
|
||||||
function renderUpgradePanel(billingState, _bootstrap) {
|
function renderUpgradePanel(billingState, _bootstrap) {
|
||||||
|
|
@ -2227,11 +2231,13 @@
|
||||||
beginMutationState(nextBillingState.upgradeCheckout);
|
beginMutationState(nextBillingState.upgradeCheckout);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
var trialRequested = billingState.upgradeTrialRequested === true && tier === "pro" && billingCycle === "monthly";
|
||||||
var data = await api.postCommercialJSON("/v1/checkout/session", {
|
var data = await api.postCommercialJSON("/v1/checkout/session", {
|
||||||
plan_key: planKey,
|
plan_key: planKey,
|
||||||
tier,
|
tier,
|
||||||
billing_cycle: billingCycle,
|
billing_cycle: billingCycle,
|
||||||
portal_handoff_id: portalHandoffID
|
portal_handoff_id: portalHandoffID,
|
||||||
|
...trialRequested ? { trial: true } : {}
|
||||||
});
|
});
|
||||||
if (!data || !data.url) {
|
if (!data || !data.url) {
|
||||||
throw new Error("Checkout URL was not returned.");
|
throw new Error("Checkout URL was not returned.");
|
||||||
|
|
@ -3590,6 +3596,10 @@
|
||||||
function normalizeUpgradeFeatureKey2(value) {
|
function normalizeUpgradeFeatureKey2(value) {
|
||||||
return String(value || "").trim();
|
return String(value || "").trim();
|
||||||
}
|
}
|
||||||
|
function normalizeUpgradeTrialRequested(value) {
|
||||||
|
var trimmed = String(value || "").trim().toLowerCase();
|
||||||
|
return trimmed === "1" || trimmed === "true";
|
||||||
|
}
|
||||||
function readPortalRuntimeHandoff(locationHref = window.location.href) {
|
function readPortalRuntimeHandoff(locationHref = window.location.href) {
|
||||||
try {
|
try {
|
||||||
var params = new URL(locationHref).searchParams;
|
var params = new URL(locationHref).searchParams;
|
||||||
|
|
@ -3602,14 +3612,16 @@
|
||||||
email: normalizeHandoffEmail(params.get("email")),
|
email: normalizeHandoffEmail(params.get("email")),
|
||||||
openBillingPanelID,
|
openBillingPanelID,
|
||||||
upgradePortalHandoffID,
|
upgradePortalHandoffID,
|
||||||
upgradeFeatureKey: normalizeUpgradeFeatureKey2(params.get("feature"))
|
upgradeFeatureKey: normalizeUpgradeFeatureKey2(params.get("feature")),
|
||||||
|
upgradeTrialRequested: normalizeUpgradeTrialRequested(params.get("trial"))
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
email: "",
|
email: "",
|
||||||
openBillingPanelID: "",
|
openBillingPanelID: "",
|
||||||
upgradePortalHandoffID: "",
|
upgradePortalHandoffID: "",
|
||||||
upgradeFeatureKey: ""
|
upgradeFeatureKey: "",
|
||||||
|
upgradeTrialRequested: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3650,12 +3662,14 @@
|
||||||
billingState.openBillingPanelID = handoff.openBillingPanelID;
|
billingState.openBillingPanelID = handoff.openBillingPanelID;
|
||||||
billingState.upgradePortalHandoffID = handoff.upgradePortalHandoffID;
|
billingState.upgradePortalHandoffID = handoff.upgradePortalHandoffID;
|
||||||
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
|
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
|
||||||
|
billingState.upgradeTrialRequested = handoff.upgradeTrialRequested;
|
||||||
}, { notify: false });
|
}, { notify: false });
|
||||||
} else if (handoff.upgradeFeatureKey || handoff.upgradePortalHandoffID) {
|
} else if (handoff.upgradeFeatureKey || handoff.upgradePortalHandoffID) {
|
||||||
store.setActiveShellSection("billing");
|
store.setActiveShellSection("billing");
|
||||||
store.updateBillingState(function(billingState) {
|
store.updateBillingState(function(billingState) {
|
||||||
billingState.upgradePortalHandoffID = handoff.upgradePortalHandoffID;
|
billingState.upgradePortalHandoffID = handoff.upgradePortalHandoffID;
|
||||||
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
|
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
|
||||||
|
billingState.upgradeTrialRequested = handoff.upgradeTrialRequested;
|
||||||
}, { notify: false });
|
}, { notify: false });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ describe('portal app', function() {
|
||||||
openBillingPanelID: 'retrieve-billing-panel',
|
openBillingPanelID: 'retrieve-billing-panel',
|
||||||
upgradePortalHandoffID: '',
|
upgradePortalHandoffID: '',
|
||||||
upgradeFeatureKey: '',
|
upgradeFeatureKey: '',
|
||||||
|
upgradeTrialRequested: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -248,11 +248,15 @@ export function installBillingRuntime(deps: BillingRuntimeDeps): void {
|
||||||
beginMutationState(nextBillingState.upgradeCheckout);
|
beginMutationState(nextBillingState.upgradeCheckout);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
var trialRequested = billingState.upgradeTrialRequested === true &&
|
||||||
|
tier === 'pro' &&
|
||||||
|
billingCycle === 'monthly';
|
||||||
var data = await api.postCommercialJSON<PortalCheckoutSessionCreateResponse>('/v1/checkout/session', {
|
var data = await api.postCommercialJSON<PortalCheckoutSessionCreateResponse>('/v1/checkout/session', {
|
||||||
plan_key: planKey,
|
plan_key: planKey,
|
||||||
tier: tier,
|
tier: tier,
|
||||||
billing_cycle: billingCycle,
|
billing_cycle: billingCycle,
|
||||||
portal_handoff_id: portalHandoffID,
|
portal_handoff_id: portalHandoffID,
|
||||||
|
...(trialRequested ? { trial: true } : {}),
|
||||||
});
|
});
|
||||||
if (!data || !data.url) {
|
if (!data || !data.url) {
|
||||||
throw new Error('Checkout URL was not returned.');
|
throw new Error('Checkout URL was not returned.');
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ function renderUpgradePlansHTML(billingState: PortalBillingState): string {
|
||||||
}).join('') + '</ul>' +
|
}).join('') + '</ul>' +
|
||||||
(plan.note ? '<div class="helper-text">' + escapeText(plan.note) + '</div>' : '') +
|
(plan.note ? '<div class="helper-text">' + escapeText(plan.note) + '</div>' : '') +
|
||||||
'<div class="form-actions">' + checkoutButtons.map(function(button) {
|
'<div class="form-actions">' + checkoutButtons.map(function(button) {
|
||||||
|
var isTrialButton = billingState.upgradeTrialRequested === true &&
|
||||||
|
button.tier === 'pro' &&
|
||||||
|
button.billingCycle === 'monthly';
|
||||||
return (
|
return (
|
||||||
'<button type="button" class="' + escapeAttribute(button.className || 'btn-primary') + '"' +
|
'<button type="button" class="' + escapeAttribute(button.className || 'btn-primary') + '"' +
|
||||||
' data-account-billing-action="upgrade-start-checkout"' +
|
' data-account-billing-action="upgrade-start-checkout"' +
|
||||||
|
|
@ -130,10 +133,15 @@ function renderUpgradePlansHTML(billingState: PortalBillingState): string {
|
||||||
' data-upgrade-billing-cycle="' + escapeAttribute(button.billingCycle || '') + '"' +
|
' data-upgrade-billing-cycle="' + escapeAttribute(button.billingCycle || '') + '"' +
|
||||||
(checkoutDisabled ? ' disabled' : '') +
|
(checkoutDisabled ? ' disabled' : '') +
|
||||||
'>' +
|
'>' +
|
||||||
escapeText(button.label) +
|
escapeText(isTrialButton ? 'Start 14-day free trial' : button.label) +
|
||||||
'</button>'
|
'</button>'
|
||||||
);
|
);
|
||||||
}).join('') + '</div>' +
|
}).join('') + '</div>' +
|
||||||
|
(billingState.upgradeTrialRequested === true && checkoutButtons.some(function(button) {
|
||||||
|
return button.tier === 'pro' && button.billingCycle === 'monthly';
|
||||||
|
})
|
||||||
|
? '<div class="helper-text">Card required. You will not be charged if you cancel during the 14-day trial.</div>'
|
||||||
|
: '') +
|
||||||
'</article>'
|
'</article>'
|
||||||
);
|
);
|
||||||
}).join('') + '</div>';
|
}).join('') + '</div>';
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,20 @@ describe('portal runtime', function() {
|
||||||
expect(handoff.openBillingPanelID).toBe('upgrade-billing-panel');
|
expect(handoff.openBillingPanelID).toBe('upgrade-billing-panel');
|
||||||
expect(handoff.upgradePortalHandoffID).toBe('cph_signed');
|
expect(handoff.upgradePortalHandoffID).toBe('cph_signed');
|
||||||
expect(handoff.upgradeFeatureKey).toBe('');
|
expect(handoff.upgradeFeatureKey).toBe('');
|
||||||
|
expect(handoff.upgradeTrialRequested).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads a trial request from the handoff arrival query', function() {
|
||||||
|
var handoff = readPortalRuntimeHandoff(
|
||||||
|
'https://cloud.pulserelay.pro/portal?portal_handoff_id=cph_signed&feature=self_hosted_plan&trial=1',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(handoff.upgradeTrialRequested).toBe(true);
|
||||||
|
|
||||||
|
var noTrial = readPortalRuntimeHandoff(
|
||||||
|
'https://cloud.pulserelay.pro/portal?portal_handoff_id=cph_signed&trial=definitely',
|
||||||
|
);
|
||||||
|
expect(noTrial.upgradeTrialRequested).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('blocks legacy checkout intent arrivals from acting as a trusted upgrade bootstrap', function() {
|
it('blocks legacy checkout intent arrivals from acting as a trusted upgrade bootstrap', function() {
|
||||||
|
|
@ -96,6 +110,7 @@ describe('portal runtime', function() {
|
||||||
openBillingPanelID: 'refund-billing-panel',
|
openBillingPanelID: 'refund-billing-panel',
|
||||||
upgradePortalHandoffID: '',
|
upgradePortalHandoffID: '',
|
||||||
upgradeFeatureKey: '',
|
upgradeFeatureKey: '',
|
||||||
|
upgradeTrialRequested: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -118,12 +133,14 @@ describe('portal runtime', function() {
|
||||||
openBillingPanelID: 'upgrade-billing-panel',
|
openBillingPanelID: 'upgrade-billing-panel',
|
||||||
upgradePortalHandoffID: 'cph_signed',
|
upgradePortalHandoffID: 'cph_signed',
|
||||||
upgradeFeatureKey: '',
|
upgradeFeatureKey: '',
|
||||||
|
upgradeTrialRequested: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(runtime.store.getShellState().activeSection).toBe('billing');
|
expect(runtime.store.getShellState().activeSection).toBe('billing');
|
||||||
expect(runtime.store.getBillingState().openBillingPanelID).toBe('upgrade-billing-panel');
|
expect(runtime.store.getBillingState().openBillingPanelID).toBe('upgrade-billing-panel');
|
||||||
expect(runtime.store.getBillingState().upgradePortalHandoffID).toBe('cph_signed');
|
expect(runtime.store.getBillingState().upgradePortalHandoffID).toBe('cph_signed');
|
||||||
|
expect(runtime.store.getBillingState().upgradeTrialRequested).toBe(true);
|
||||||
expect(runtime.store.getBillingState().upgradeFeatureKey).toBe('');
|
expect(runtime.store.getBillingState().upgradeFeatureKey).toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export interface PortalRuntimeHandoff {
|
||||||
openBillingPanelID: string;
|
openBillingPanelID: string;
|
||||||
upgradePortalHandoffID: string;
|
upgradePortalHandoffID: string;
|
||||||
upgradeFeatureKey: string;
|
upgradeFeatureKey: string;
|
||||||
|
upgradeTrialRequested: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PortalRuntime {
|
export interface PortalRuntime {
|
||||||
|
|
@ -61,6 +62,11 @@ function normalizeUpgradeFeatureKey(value: string | null): string {
|
||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeUpgradeTrialRequested(value: string | null): boolean {
|
||||||
|
var trimmed = String(value || '').trim().toLowerCase();
|
||||||
|
return trimmed === '1' || trimmed === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
export function readPortalRuntimeHandoff(
|
export function readPortalRuntimeHandoff(
|
||||||
locationHref: string | undefined = window.location.href,
|
locationHref: string | undefined = window.location.href,
|
||||||
): PortalRuntimeHandoff {
|
): PortalRuntimeHandoff {
|
||||||
|
|
@ -76,6 +82,7 @@ export function readPortalRuntimeHandoff(
|
||||||
openBillingPanelID: openBillingPanelID,
|
openBillingPanelID: openBillingPanelID,
|
||||||
upgradePortalHandoffID: upgradePortalHandoffID,
|
upgradePortalHandoffID: upgradePortalHandoffID,
|
||||||
upgradeFeatureKey: normalizeUpgradeFeatureKey(params.get('feature')),
|
upgradeFeatureKey: normalizeUpgradeFeatureKey(params.get('feature')),
|
||||||
|
upgradeTrialRequested: normalizeUpgradeTrialRequested(params.get('trial')),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
|
|
@ -83,6 +90,7 @@ export function readPortalRuntimeHandoff(
|
||||||
openBillingPanelID: '',
|
openBillingPanelID: '',
|
||||||
upgradePortalHandoffID: '',
|
upgradePortalHandoffID: '',
|
||||||
upgradeFeatureKey: '',
|
upgradeFeatureKey: '',
|
||||||
|
upgradeTrialRequested: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,12 +140,14 @@ export function createPortalRuntime(
|
||||||
billingState.openBillingPanelID = handoff.openBillingPanelID;
|
billingState.openBillingPanelID = handoff.openBillingPanelID;
|
||||||
billingState.upgradePortalHandoffID = handoff.upgradePortalHandoffID;
|
billingState.upgradePortalHandoffID = handoff.upgradePortalHandoffID;
|
||||||
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
|
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
|
||||||
|
billingState.upgradeTrialRequested = handoff.upgradeTrialRequested;
|
||||||
}, { notify: false });
|
}, { notify: false });
|
||||||
} else if (handoff.upgradeFeatureKey || handoff.upgradePortalHandoffID) {
|
} else if (handoff.upgradeFeatureKey || handoff.upgradePortalHandoffID) {
|
||||||
store.setActiveShellSection('billing');
|
store.setActiveShellSection('billing');
|
||||||
store.updateBillingState(function(billingState) {
|
store.updateBillingState(function(billingState) {
|
||||||
billingState.upgradePortalHandoffID = handoff.upgradePortalHandoffID;
|
billingState.upgradePortalHandoffID = handoff.upgradePortalHandoffID;
|
||||||
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
|
billingState.upgradeFeatureKey = handoff.upgradeFeatureKey;
|
||||||
|
billingState.upgradeTrialRequested = handoff.upgradeTrialRequested;
|
||||||
}, { notify: false });
|
}, { notify: false });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export function createPortalBillingState(): PortalBillingState {
|
||||||
return {
|
return {
|
||||||
openBillingPanelID: '',
|
openBillingPanelID: '',
|
||||||
upgradeFeatureKey: '',
|
upgradeFeatureKey: '',
|
||||||
|
upgradeTrialRequested: false,
|
||||||
upgradePortalHandoffID: '',
|
upgradePortalHandoffID: '',
|
||||||
upgradePortalHandoff: createQueryState(null),
|
upgradePortalHandoff: createQueryState(null),
|
||||||
upgradePricing: createQueryState(null),
|
upgradePricing: createQueryState(null),
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@ export interface RefundState {
|
||||||
export interface PortalBillingState {
|
export interface PortalBillingState {
|
||||||
openBillingPanelID: string;
|
openBillingPanelID: string;
|
||||||
upgradeFeatureKey: string;
|
upgradeFeatureKey: string;
|
||||||
|
upgradeTrialRequested: boolean;
|
||||||
upgradePortalHandoffID: string;
|
upgradePortalHandoffID: string;
|
||||||
upgradePortalHandoff: PortalQueryState<PortalUpgradePortalHandoffModel | null>;
|
upgradePortalHandoff: PortalQueryState<PortalUpgradePortalHandoffModel | null>;
|
||||||
upgradePricing: PortalQueryState<PortalUpgradePricingModel | null>;
|
upgradePricing: PortalQueryState<PortalUpgradePricingModel | null>;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue