mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Centralize trial CTA action handling
This commit is contained in:
@@ -191,6 +191,12 @@ The dedicated profile client now also routes list, schema, and validation
|
||||
parsing through shared response helpers in `frontend-modern/src/api/agentProfiles.ts`,
|
||||
so profile transport stays aligned with the governed API contract instead of
|
||||
reintroducing local array or JSON parsing rules.
|
||||
That same lifecycle-owned install/profile surface now also routes trial-start
|
||||
CTA orchestration through `frontend-modern/src/utils/trialStartAction.ts`.
|
||||
Agent profile paywalls, NodeModal upgrade prompts, and setup-completion trial
|
||||
actions may choose their own success copy, but they must use the shared helper
|
||||
for hosted trial redirects and canonical denial handling instead of open-coding
|
||||
`startProTrial()` branches in each lifecycle surface.
|
||||
|
||||
The owned backend API surfaces must preserve the exact-release installer
|
||||
fallback, canonical /api/auto-register behavior, and hosted org install-command
|
||||
|
||||
@@ -203,6 +203,12 @@ The same facet bundle contract now also returns grouped
|
||||
`recentChangeSourceAdapters` counts by canonical source adapter, so the
|
||||
shared drawer and summary chips can distinguish Docker, Proxmox, TrueNAS, and
|
||||
ops-helper provenance without inventing frontend-local integration heuristics.
|
||||
Client consumers of the node setup transport now also share the canonical
|
||||
trial-start action helper in `frontend-modern/src/utils/trialStartAction.ts`
|
||||
for the NodeModal Pro upgrade path. The NodesAPI client remains the source of
|
||||
truth for setup/install requests, while hosted trial redirects and denial copy
|
||||
must flow through the shared trial-start owner rather than a second client-side
|
||||
status-code map inside node setup state.
|
||||
Canonical timeline entries now also preserve correlation context in
|
||||
`relatedResources`, so the history surface can explain which neighboring
|
||||
resources moved with restart, anomaly, config, state transition, and
|
||||
|
||||
@@ -231,6 +231,13 @@ The top-level authenticated shell is part of that same customer-facing
|
||||
boundary: cloud-paid trial prompts may appear in owned commercial surfaces, but
|
||||
the app shell must not force a global, persistent Pro trial nudge that
|
||||
overrides the primary runtime chrome for every signed-in user.
|
||||
The shared trial-start runtime is part of that same cloud-paid boundary.
|
||||
Commercial relay, onboarding, setup, Pro settings, and shared paywall
|
||||
surfaces may customize success copy, but they must route hosted handoff,
|
||||
success-notification, and canonical denial handling through
|
||||
`frontend-modern/src/utils/trialStartAction.ts` instead of carrying local
|
||||
`startProTrial()` redirect/error branches that drift from backend commercial
|
||||
truth.
|
||||
The hosted trial handoff page is part of that same boundary as well. It may
|
||||
still use a secure hosted Stripe-backed session internally, but the customer
|
||||
copy must present the flow as starting a trial for the originating Pulse
|
||||
|
||||
@@ -210,8 +210,12 @@ Shared trial CTA handling is now part of that same primitive boundary for
|
||||
settings and shared paywalls. Shared/settings runtime owners must derive trial
|
||||
eligibility from the canonical entitlements payload, including
|
||||
`trial_eligible`, and route operator-facing failure copy through
|
||||
`frontend-modern/src/utils/upgradePresentation.ts` instead of open-coding local
|
||||
409/429 branches that drift from backend denial reasons.
|
||||
`frontend-modern/src/utils/upgradePresentation.ts`. The trial-start runtime
|
||||
handoff itself is now centralized in
|
||||
`frontend-modern/src/utils/trialStartAction.ts`; settings/shared paywalls and
|
||||
onboarding surfaces must use that owner for redirect, success-notification, and
|
||||
canonical denial handling instead of open-coding local `startProTrial()`
|
||||
branches or re-interpreting backend status codes.
|
||||
Top-level route files are now also expected to stay thin when a feature owns
|
||||
the real product surface. `frontend-modern/src/pages/Infrastructure.tsx` now
|
||||
acts only as the route boundary, while
|
||||
|
||||
@@ -160,3 +160,8 @@ through the canonical upgrade presentation helper instead of collapsing every
|
||||
trial-start conflict into a generic already-used message. Organization settings
|
||||
paywalls should only map the explicit canonical trial helper outputs, not
|
||||
re-interpret status codes locally.
|
||||
The RBAC feature-gate state now also depends on the shared
|
||||
`frontend-modern/src/utils/trialStartAction.ts` owner for hosted handoff and
|
||||
success/error orchestration. Organization settings paywalls must not keep a
|
||||
lane-local `startProTrial()` branch once that shared helper covers the same
|
||||
runtime contract.
|
||||
|
||||
@@ -213,6 +213,12 @@ The same page and drawer now also share the canonical
|
||||
`frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx`
|
||||
card for recent changes, so the timeline layout and relative-time wording
|
||||
stay governed by one frontend feed instead of separate page-local loops.
|
||||
Patrol trial-entry surfaces now also share the canonical
|
||||
`frontend-modern/src/utils/trialStartAction.ts` owner for hosted handoff and
|
||||
denial handling. `ApprovalSection.tsx` and
|
||||
`usePatrolIntelligenceState.ts` may still choose Patrol-specific success copy,
|
||||
but they must not reintroduce local `startProTrial()` status-code branches
|
||||
that diverge from the commercial backend contract.
|
||||
That same store now owns the Patrol dashboard load bundle as well, so the
|
||||
page refresh path stays aligned on a single orchestrated AI bundle instead of
|
||||
repeating the individual summary, findings, approval, and correlation fetches
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import relayOnboardingCardStateSource from '../useRelayOnboardingCardState.ts?raw';
|
||||
|
||||
// ── Hoisted mocks ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -125,6 +126,11 @@ function setupWithoutRelayFeature() {
|
||||
// ── Tests ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('RelayOnboardingCard', () => {
|
||||
it('keeps relay onboarding trial flow on the shared trial action owner', () => {
|
||||
expect(relayOnboardingCardStateSource).toContain('runStartProTrialAction({');
|
||||
expect(relayOnboardingCardStateSource).not.toContain('startProTrial()');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllMocks();
|
||||
});
|
||||
|
||||
@@ -5,13 +5,12 @@ import {
|
||||
hasFeature,
|
||||
licenseLoaded,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { isUpsellSnoozed, snoozeUpsell } from '@/utils/snooze';
|
||||
import { showError, showSuccess } from '@/utils/toast';
|
||||
import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics';
|
||||
import { getTrialStartErrorMessage } from '@/utils/upgradePresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
const SNOOZE_KEY = 'pulse_relay_onboarding_snoozed';
|
||||
const RELAY_SETTINGS_PATH = '/settings/system-relay';
|
||||
@@ -109,21 +108,17 @@ export function useRelayOnboardingCardState() {
|
||||
|
||||
setTrialStarting(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = result.actionUrl;
|
||||
}
|
||||
return;
|
||||
const outcome = await runStartProTrialAction({
|
||||
branded: true,
|
||||
successMessage: 'Trial started. Relay is now available.',
|
||||
showSuccess,
|
||||
showError,
|
||||
});
|
||||
if (outcome === 'activated') {
|
||||
await loadLicenseStatus(true);
|
||||
setStatusLoaded(false);
|
||||
void loadRelayStatusOnce();
|
||||
}
|
||||
|
||||
showSuccess('Trial started. Relay is now available.');
|
||||
await loadLicenseStatus(true);
|
||||
setStatusLoaded(false);
|
||||
void loadRelayStatusOnce();
|
||||
} catch (error) {
|
||||
logger.warn('[RelayOnboardingCard] Failed to start trial', error);
|
||||
showError(getTrialStartErrorMessage(error, { branded: true }));
|
||||
} finally {
|
||||
setTrialStarting(false);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ describe('NodeModal guardrails', () => {
|
||||
expect(nodeModalSetupGuideSectionSource).toContain("await state.copyProxmoxAgentInstallCommand(");
|
||||
expect(nodeModalMonitoringSectionSource).toContain('title="Monitoring coverage"');
|
||||
expect(nodeModalStatusFooterSource).toContain('Start your free 14-day trial');
|
||||
expect(nodeModalStateSource).toContain('runStartProTrialAction({');
|
||||
expect(nodeModalStateSource).not.toContain('startProTrial()');
|
||||
});
|
||||
|
||||
it('keeps the manual PVE permission snippet aligned with the canonical setup script', () => {
|
||||
|
||||
@@ -454,6 +454,8 @@ describe('ProLicensePanel', () => {
|
||||
expect(proLicensePanelStateSource).toContain('useLocation');
|
||||
expect(proLicensePanelStateSource).toContain('loadLicenseStatus(true)');
|
||||
expect(proLicensePanelStateSource).toContain('buildSelfHostedCommercialPlanModel');
|
||||
expect(proLicensePanelStateSource).toContain('runStartProTrialAction({');
|
||||
expect(proLicensePanelStateSource).not.toContain('startProTrial()');
|
||||
expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState');
|
||||
expect(proLicensePlanSectionSource).toContain('getNoActiveProLicenseState');
|
||||
});
|
||||
|
||||
+4
-2
@@ -466,7 +466,8 @@ describe('monitored-system model guardrails', () => {
|
||||
expect(relayOnboardingCardStateSource).toContain('loadLicenseStatus()');
|
||||
expect(relayOnboardingCardStateSource).toContain('RelayAPI.getStatus()');
|
||||
expect(relayOnboardingCardStateSource).toContain('trackPaywallViewed');
|
||||
expect(relayOnboardingCardStateSource).toContain('startProTrial()');
|
||||
expect(relayOnboardingCardStateSource).toContain('runStartProTrialAction({');
|
||||
expect(relayOnboardingCardStateSource).not.toContain('startProTrial()');
|
||||
expect(infrastructureInstallStateSource).toContain('STORAGE_KEYS.SETUP_HANDOFF');
|
||||
expect(infrastructureInstallerSectionSource).toContain(
|
||||
'Security configured. Save these first-run credentials now.',
|
||||
@@ -1242,7 +1243,8 @@ describe('monitored-system model guardrails', () => {
|
||||
expect(rolesEditorDialogSource).toContain('RBAC_PERMISSION_RESOURCES');
|
||||
expect(rbacFeatureGateSectionSource).toContain('trackUpgradeClicked');
|
||||
expect(rbacFeatureGateStateSource).toContain('trackPaywallViewed');
|
||||
expect(rbacFeatureGateStateSource).toContain('startProTrial');
|
||||
expect(rbacFeatureGateStateSource).toContain('runStartProTrialAction({');
|
||||
expect(rbacFeatureGateStateSource).not.toContain('startProTrial()');
|
||||
expect(userAssignmentsDialogSource).toContain('Effective Permissions Preview');
|
||||
expect(userAssignmentsPanelStateSource).toContain('RBACAPI.getUsers');
|
||||
expect(userAssignmentsPanelStateSource).toContain('RBACAPI.updateUserRoles');
|
||||
|
||||
@@ -529,6 +529,8 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(monitoredSystemDefinitionDisclosureSource).not.toContain('{props.summary}');
|
||||
expect(proLicensePanelStateSource).toContain('buildSelfHostedCommercialPlanModel');
|
||||
expect(proLicensePanelStateSource).toContain('loadLicenseStatus(true)');
|
||||
expect(proLicensePanelStateSource).toContain('runStartProTrialAction({');
|
||||
expect(proLicensePanelStateSource).not.toContain('startProTrial()');
|
||||
expect(proLicensePlanSectionSource).toContain('CommercialStatGrid');
|
||||
expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState');
|
||||
expect(monitoredSystemPresentationSource).toContain('export function getMonitoredSystemLedgerPresentation');
|
||||
@@ -568,6 +570,8 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(relaySettingsPanelStateSource).toContain("trackPaywallViewed('relay', 'settings_relay_panel')");
|
||||
expect(relaySettingsPanelStateSource).toContain('setInterval(() => void loadStatus(), 5000)');
|
||||
expect(relaySettingsPanelStateSource).toContain('QRCode.toDataURL(payload.deep_link');
|
||||
expect(relaySettingsPanelStateSource).toContain('runStartProTrialAction({');
|
||||
expect(relaySettingsPanelStateSource).not.toContain('startProTrial()');
|
||||
expect(relayPairingSectionSource).toContain('getRelayDiagnosticClass');
|
||||
expect(relayPairingSectionSource).toContain('Pair New Device');
|
||||
});
|
||||
@@ -757,7 +761,8 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(userAssignmentsPanelSource).not.toContain('const loadData = async');
|
||||
expect(rbacFeatureGateSectionSource).toContain('trackUpgradeClicked');
|
||||
expect(rbacFeatureGateStateSource).toContain('trackPaywallViewed');
|
||||
expect(rbacFeatureGateStateSource).toContain('startProTrial');
|
||||
expect(rbacFeatureGateStateSource).toContain('runStartProTrialAction({');
|
||||
expect(rbacFeatureGateStateSource).not.toContain('startProTrial()');
|
||||
expect(rolesEditorDialogSource).toContain('RBAC_PERMISSION_ACTIONS');
|
||||
expect(rolesPanelStateSource).toContain('RBACAPI.getRoles');
|
||||
expect(rolesPanelStateSource).toContain('RBACAPI.saveRole');
|
||||
@@ -948,6 +953,8 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(nodeModalStateSource).toContain('const [quickSetupBootstrap, setQuickSetupBootstrap] =');
|
||||
expect(nodeModalStateSource).toContain('const handleTestConnection = async () =>');
|
||||
expect(nodeModalStateSource).toContain("const PROXMOX_SETUP_HOST_REQUIRED_MESSAGE = 'Proxmox setup host is required';");
|
||||
expect(nodeModalStateSource).toContain('runStartProTrialAction({');
|
||||
expect(nodeModalStateSource).not.toContain('startProTrial()');
|
||||
});
|
||||
|
||||
it('keeps AI settings sub-surfaces behind extracted runtime owners', () => {
|
||||
@@ -980,7 +987,8 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(aiSettingsStateSource).toContain('const handleSave = async (event?: Event) =>');
|
||||
expect(aiSettingsStateSource).toContain('const handleEnabledToggle = async (newValue: boolean) =>');
|
||||
expect(aiSettingsStateSource).toContain('AIAPI.getSettings()');
|
||||
expect(aiSettingsStateSource).toContain('showWarning(getTrialStartErrorMessage(error));');
|
||||
expect(aiSettingsStateSource).toContain('runStartProTrialAction({');
|
||||
expect(aiSettingsStateSource).not.toContain('startProTrial()');
|
||||
expect(aiSettingsStateSource).not.toContain('getTrialAlreadyUsedMessage()');
|
||||
});
|
||||
|
||||
@@ -1034,10 +1042,10 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(reportingPanelSource).not.toContain('window.URL.createObjectURL');
|
||||
expect(reportingPanelStateSource).toContain('export const useReportingPanelState =');
|
||||
expect(reportingPanelStateSource).toContain('loadLicenseStatus');
|
||||
expect(reportingPanelStateSource).toContain('startProTrial');
|
||||
expect(reportingPanelStateSource).toContain('runStartProTrialAction({');
|
||||
expect(reportingPanelStateSource).not.toContain('startProTrial()');
|
||||
expect(reportingPanelStateSource).toContain('buildReportingRequest');
|
||||
expect(reportingPanelStateSource).toContain('getReportingGenerateSuccessMessage');
|
||||
expect(reportingPanelStateSource).toContain('showWarning(getTrialStartErrorMessage(error));');
|
||||
expect(reportingPanelStateSource).not.toContain('getTrialAlreadyUsedMessage()');
|
||||
expect(reportingPanelModelSource).toContain('export function getReportingRangeStart');
|
||||
expect(reportingPanelModelSource).toContain('export function buildReportingRequest');
|
||||
@@ -1063,7 +1071,8 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(auditLogStateSource).toContain('const fetchAuditEvents = async (');
|
||||
expect(auditLogStateSource).toContain('const verifyAllEvents = async (');
|
||||
expect(auditLogStateSource).toContain('trackPaywallViewed');
|
||||
expect(auditLogStateSource).toContain('showWarning(getTrialStartErrorMessage(err));');
|
||||
expect(auditLogStateSource).toContain('runStartProTrialAction({');
|
||||
expect(auditLogStateSource).not.toContain('startProTrial()');
|
||||
expect(auditLogStateSource).not.toContain('getTrialAlreadyUsedMessage()');
|
||||
});
|
||||
|
||||
@@ -1078,7 +1087,8 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(auditWebhookStateSource).toContain('const fetchWebhooks = async () =>');
|
||||
expect(auditWebhookStateSource).toContain('const saveWebhooks = async (urls: string[]) =>');
|
||||
expect(auditWebhookStateSource).toContain('trackPaywallViewed');
|
||||
expect(auditWebhookStateSource).toContain('showWarning(getTrialStartErrorMessage(error));');
|
||||
expect(auditWebhookStateSource).toContain('runStartProTrialAction({');
|
||||
expect(auditWebhookStateSource).not.toContain('startProTrial()');
|
||||
expect(auditWebhookStateSource).not.toContain('getTrialAlreadyUsedMessage()');
|
||||
});
|
||||
|
||||
@@ -1094,9 +1104,8 @@ describe('Settings architecture guardrails', () => {
|
||||
expect(ssoProvidersStateSource).toContain('const loadProviders = async () =>');
|
||||
expect(ssoProvidersStateSource).toContain('const handleSave = async (event?: Event) =>');
|
||||
expect(ssoProvidersStateSource).toContain('const testConnection = async () =>');
|
||||
expect(ssoProvidersStateSource).toContain(
|
||||
'notificationStore.error(getTrialStartErrorMessage(err));',
|
||||
);
|
||||
expect(ssoProvidersStateSource).toContain('runStartProTrialAction({');
|
||||
expect(ssoProvidersStateSource).not.toContain('startProTrial()');
|
||||
expect(ssoProvidersStateSource).not.toContain('getTrialAlreadyUsedMessage()');
|
||||
expect(ssoProvidersModelSource).toContain('export const createEmptyProviderForm =');
|
||||
expect(ssoProvidersModelSource).toContain('export const mapProviderDetailsToForm =');
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
getUpgradeActionUrlOrFallback,
|
||||
hasFeature,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import type { AISettings as AISettingsType, AIProvider, AuthMethod } from '@/types/ai';
|
||||
@@ -38,11 +37,8 @@ import {
|
||||
} from '@/utils/aiSettingsPresentation';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { showSuccess, showWarning } from '@/utils/toast';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
|
||||
export const useAISettingsState = () => {
|
||||
const [settings, setSettings] = createSignal<AISettingsType | null>(null);
|
||||
@@ -278,14 +274,10 @@ export const useAISettingsState = () => {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
window.location.href = result.actionUrl;
|
||||
return;
|
||||
}
|
||||
showSuccess(getProTrialStartedMessage());
|
||||
} catch (error) {
|
||||
showWarning(getTrialStartErrorMessage(error));
|
||||
await runStartProTrialAction({
|
||||
showSuccess,
|
||||
showError: showWarning,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -17,22 +17,19 @@ import {
|
||||
licenseLoaded,
|
||||
licenseLoading,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import type { ConnectedInfrastructureItem } from '@/types/api';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import { logger } from '@/utils/logger';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialAlreadyUsedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
getUpgradeActionButtonClass,
|
||||
UPGRADE_ACTION_LABEL,
|
||||
UPGRADE_TRIAL_LABEL,
|
||||
UPGRADE_TRIAL_LINK_CLASS,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
import { KNOWN_SETTINGS } from './agentProfileSettings';
|
||||
import {
|
||||
getActionableAgentIdFromResource,
|
||||
@@ -218,21 +215,10 @@ export const useAgentProfilesPanelState = () => {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
window.location.href = result.actionUrl;
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (err) {
|
||||
const statusCode = (err as { status?: number } | null)?.status;
|
||||
if (statusCode === 409) {
|
||||
notificationStore.error(getTrialAlreadyUsedMessage());
|
||||
} else {
|
||||
notificationStore.error(
|
||||
getTrialStartErrorMessage(err instanceof Error ? err.message : undefined),
|
||||
);
|
||||
}
|
||||
await runStartProTrialAction({
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,9 @@ import {
|
||||
hasFeature,
|
||||
licenseLoaded,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
export interface AuditEvent {
|
||||
id: string;
|
||||
@@ -526,14 +522,10 @@ export const useAuditLogPanelState = () => {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
window.location.href = result.actionUrl;
|
||||
return;
|
||||
}
|
||||
showSuccess(getProTrialStartedMessage());
|
||||
} catch (err) {
|
||||
showWarning(getTrialStartErrorMessage(err));
|
||||
await runStartProTrialAction({
|
||||
showSuccess,
|
||||
showError: showWarning,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -8,19 +8,15 @@ import {
|
||||
hasFeature,
|
||||
licenseLoaded,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import {
|
||||
getAuditWebhookDuplicateUrlMessage,
|
||||
getAuditWebhookInvalidUrlMessage,
|
||||
getAuditWebhookSaveErrorMessage,
|
||||
getAuditWebhookSaveSuccessMessage,
|
||||
} from '@/utils/auditWebhookPresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
export const useAuditWebhookPanelState = (canManageOverride?: boolean) => {
|
||||
const [webhookUrls, setWebhookUrls] = createSignal<string[]>([]);
|
||||
@@ -38,14 +34,10 @@ export const useAuditWebhookPanelState = (canManageOverride?: boolean) => {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
window.location.href = result.actionUrl;
|
||||
return;
|
||||
}
|
||||
showSuccess(getProTrialStartedMessage());
|
||||
} catch (error) {
|
||||
showWarning(getTrialStartErrorMessage(error));
|
||||
await runStartProTrialAction({
|
||||
showSuccess,
|
||||
showError: showWarning,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NodeConfig } from '@/types/nodes';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { NodesAPI } from '@/api/nodes';
|
||||
import type { ProxmoxSetupCommandResponse } from '@/api/nodes';
|
||||
import { licenseStatus, startProTrial } from '@/stores/license';
|
||||
import { licenseStatus } from '@/stores/license';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { logger } from '@/utils/logger';
|
||||
import {
|
||||
@@ -13,12 +13,7 @@ import {
|
||||
getNodeModalTestResultPresentation,
|
||||
type NodeModalFormData,
|
||||
} from '@/utils/nodeModalPresentation';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialAlreadyUsedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
getTrialTryAgainLaterMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
import { deriveNameFromHost, type NodeModalProps } from './nodeModalModel';
|
||||
|
||||
@@ -71,27 +66,11 @@ export const useNodeModalState = (props: NodeModalProps) => {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = result.actionUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (err) {
|
||||
const statusCode = (err as { status?: number } | null)?.status;
|
||||
if (statusCode === 409) {
|
||||
notificationStore.error(getTrialAlreadyUsedMessage());
|
||||
} else if (statusCode === 429) {
|
||||
notificationStore.error(getTrialTryAgainLaterMessage());
|
||||
} else {
|
||||
notificationStore.error(
|
||||
getTrialStartErrorMessage(err instanceof Error ? err.message : undefined, {
|
||||
branded: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await runStartProTrialAction({
|
||||
branded: true,
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,8 @@ import {
|
||||
licenseLoadError,
|
||||
licenseStatus,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { LicenseAPI } from '@/api/license';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import {
|
||||
formatLicensePlanVersion,
|
||||
getCommercialMigrationNotice,
|
||||
@@ -23,6 +18,7 @@ import {
|
||||
getTrialActivationNotice,
|
||||
} from '@/utils/licensePresentation';
|
||||
import { buildSelfHostedCommercialPlanModel } from '@/utils/commercialBillingModel';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
const formatDate = (value?: string | null) => {
|
||||
if (!value) return 'Not available';
|
||||
@@ -76,16 +72,11 @@ export function useProLicensePanelState() {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = result.actionUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (error) {
|
||||
notificationStore.error(getTrialStartErrorMessage(error, { branded: true }));
|
||||
await runStartProTrialAction({
|
||||
branded: true,
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,11 @@ import {
|
||||
hasFeature,
|
||||
licenseLoaded,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { getRBACFeatureGateCopy, type RBACFeatureGateCopy } from '@/utils/rbacPresentation';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
export type RBACFeatureGateKind = 'roles' | 'user-assignments';
|
||||
export type RBACFeatureGateLocation =
|
||||
@@ -54,14 +50,10 @@ export function useRBACFeatureGateState(options: UseRBACFeatureGateStateOptions)
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
window.location.href = result.actionUrl;
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (err) {
|
||||
notificationStore.error(getTrialStartErrorMessage(err));
|
||||
await runStartProTrialAction({
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
hasFeature,
|
||||
licenseLoaded,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import { showError, showSuccess } from '@/utils/toast';
|
||||
@@ -14,6 +13,7 @@ import { OnboardingAPI, type OnboardingQRResponse } from '@/api/onboarding';
|
||||
import { SecurityAPI, type APITokenRecord } from '@/api/security';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { getRelayConnectionPresentation } from '@/utils/relayPresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
export interface RelaySettingsPanelProps {
|
||||
@@ -153,19 +153,11 @@ export function useRelaySettingsPanelState(props: RelaySettingsPanelProps) {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
window.location.href = result.actionUrl;
|
||||
return;
|
||||
}
|
||||
showSuccess('Remote access trial started');
|
||||
} catch (error) {
|
||||
const statusCode = (error as { status?: number } | null)?.status;
|
||||
if (statusCode === 409) {
|
||||
showError('Trial already used');
|
||||
} else {
|
||||
showError(error instanceof Error ? error.message : 'Failed to start trial');
|
||||
}
|
||||
await runStartProTrialAction({
|
||||
successMessage: 'Remote access trial started',
|
||||
showSuccess,
|
||||
showError,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -8,18 +8,14 @@ import {
|
||||
hasFeature,
|
||||
licenseLoaded,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import {
|
||||
getReportingGenerateErrorMessage,
|
||||
getReportingGenerateSelectionRequiredMessage,
|
||||
getReportingGenerateSuccessMessage,
|
||||
} from '@/utils/reportingPresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
import {
|
||||
buildReportingRequest,
|
||||
getReportingRangeStart,
|
||||
@@ -57,14 +53,10 @@ export const useReportingPanelState = () => {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
window.location.href = result.actionUrl;
|
||||
return;
|
||||
}
|
||||
showSuccess(getProTrialStartedMessage());
|
||||
} catch (error) {
|
||||
showWarning(getTrialStartErrorMessage(error));
|
||||
await runStartProTrialAction({
|
||||
showSuccess,
|
||||
showError: showWarning,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
hasFeature,
|
||||
loadLicenseStatus,
|
||||
licenseLoaded,
|
||||
startProTrial,
|
||||
entitlements,
|
||||
} from '@/stores/license';
|
||||
import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics';
|
||||
@@ -28,10 +27,7 @@ import {
|
||||
getSSOProvidersLoadErrorMessage,
|
||||
getSSOTestResultPresentation,
|
||||
} from '@/utils/ssoProviderPresentation';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
import type {
|
||||
MetadataPreview,
|
||||
ProviderForm,
|
||||
@@ -81,14 +77,10 @@ export const useSSOProvidersState = (props: SSOProvidersPanelProps) => {
|
||||
}
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
window.location.href = result.actionUrl;
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (err) {
|
||||
notificationStore.error(getTrialStartErrorMessage(err));
|
||||
await runStartProTrialAction({
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
import {
|
||||
loadLicenseStatus,
|
||||
entitlements,
|
||||
getUpgradeActionUrlOrFallback,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import {
|
||||
RELAY_ONBOARDING_SETUP_LABEL,
|
||||
@@ -33,6 +31,7 @@ import {
|
||||
RELAY_ONBOARDING_TRIAL_HINT,
|
||||
RELAY_ONBOARDING_TRIAL_STARTING_LABEL,
|
||||
} from '@/utils/relayPresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
import type { WizardState } from '../SetupWizard';
|
||||
|
||||
interface CompleteStepProps {
|
||||
@@ -299,23 +298,15 @@ Keep these credentials secure!
|
||||
|
||||
setTrialStarting(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = result.actionUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccess('14-day Pro trial started! Set up Relay to monitor from your phone.');
|
||||
setTrialStarted(true);
|
||||
await loadLicenseStatus(true);
|
||||
} catch (err) {
|
||||
logger.warn('[SetupCompletionPanel] Failed to start trial; falling back to upgrade URL', err);
|
||||
showError('Unable to start trial. Redirecting to upgrade options...');
|
||||
const upgradeUrl = getUpgradeActionUrlOrFallback('relay');
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = upgradeUrl;
|
||||
const outcome = await runStartProTrialAction({
|
||||
branded: true,
|
||||
successMessage: '14-day Pro trial started! Set up Relay to monitor from your phone.',
|
||||
showSuccess,
|
||||
showError,
|
||||
});
|
||||
if (outcome === 'activated') {
|
||||
setTrialStarted(true);
|
||||
await loadLicenseStatus(true);
|
||||
}
|
||||
} finally {
|
||||
setTrialStarting(false);
|
||||
|
||||
+2
@@ -12,6 +12,8 @@ describe('SetupCompletionPanel guardrails', () => {
|
||||
expect(setupCompletionPanelSource).toContain('Use the Infrastructure Install workspace to:');
|
||||
expect(setupCompletionPanelSource).toContain('generate Unified Agent tokens');
|
||||
expect(setupCompletionPanelSource).toContain('configure TLS and custom CA options');
|
||||
expect(setupCompletionPanelSource).toContain('runStartProTrialAction({');
|
||||
expect(setupCompletionPanelSource).not.toContain('getUpgradeActionUrlOrFallback');
|
||||
});
|
||||
|
||||
it('describes setup completion through the unified resource model instead of legacy install-command copy', () => {
|
||||
|
||||
@@ -9,16 +9,11 @@ import { Component, Show, createSignal, createResource, createMemo } from 'solid
|
||||
import { aiIntelligenceStore } from '@/stores/aiIntelligence';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { aiChatStore } from '@/stores/aiChat';
|
||||
import { hasFeature, licenseStatus, startProTrial } from '@/stores/license';
|
||||
import { hasFeature, licenseStatus } from '@/stores/license';
|
||||
import { AIAPI, type ApprovalRequest, type ApprovalExecutionResult } from '@/api/ai';
|
||||
import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation';
|
||||
import { RemediationStatus } from './RemediationStatus';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialAlreadyUsedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
getTrialTryAgainLaterMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
interface ApprovalSectionProps {
|
||||
findingId: string;
|
||||
@@ -59,27 +54,11 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = result.actionUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (err) {
|
||||
const statusCode = (err as { status?: number } | null)?.status;
|
||||
if (statusCode === 409) {
|
||||
notificationStore.error(getTrialAlreadyUsedMessage());
|
||||
} else if (statusCode === 429) {
|
||||
notificationStore.error(getTrialTryAgainLaterMessage());
|
||||
} else {
|
||||
notificationStore.error(
|
||||
getTrialStartErrorMessage(err instanceof Error ? err.message : undefined, {
|
||||
branded: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await runStartProTrialAction({
|
||||
branded: true,
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,8 @@ describe('ActiveUseTrialNudge', () => {
|
||||
expect(activeUseTrialNudgeStateSource).toContain('createMemo');
|
||||
expect(activeUseTrialNudgeStateSource).toContain('window.localStorage');
|
||||
expect(activeUseTrialNudgeStateSource).toContain('setInterval');
|
||||
expect(activeUseTrialNudgeStateSource).toContain('startProTrial');
|
||||
expect(activeUseTrialNudgeStateSource).toContain('runStartProTrialAction');
|
||||
expect(activeUseTrialNudgeStateSource).not.toContain('startProTrial()');
|
||||
expect(activeUseTrialNudgeStateSource).toContain('snoozeUpsell');
|
||||
|
||||
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY');
|
||||
|
||||
@@ -69,9 +69,8 @@ describe('HistoryChart', () => {
|
||||
expect(historyChartStateSource).toContain('export function useHistoryChartState');
|
||||
expect(historyChartStateSource).toContain('HISTORY_CHART_RANGES');
|
||||
expect(historyChartStateSource).toContain('return ent.trial_eligible !== false;');
|
||||
expect(historyChartStateSource).toContain(
|
||||
'notificationStore.error(getTrialStartErrorMessage(err, { branded: true }));',
|
||||
);
|
||||
expect(historyChartStateSource).toContain('runStartProTrialAction({');
|
||||
expect(historyChartStateSource).not.toContain('startProTrial()');
|
||||
expect(historyChartStateSource).not.toContain('getTrialAlreadyUsedMessage()');
|
||||
expect(historyChartStateSource).not.toContain('getTrialTryAgainLaterMessage()');
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { createMemo, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { licenseStatus, startProTrial } from '@/stores/license';
|
||||
import { licenseStatus } from '@/stores/license';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { isUpsellSnoozed, snoozeUpsell } from '@/utils/snooze';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialAlreadyUsedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
getTrialTryAgainLaterMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
import {
|
||||
ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY,
|
||||
ACTIVE_USE_TRIAL_NUDGE_REFRESH_MS,
|
||||
@@ -66,27 +61,11 @@ export function useActiveUseTrialNudgeState() {
|
||||
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = result.actionUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (error) {
|
||||
const statusCode = (error as { status?: number } | null)?.status;
|
||||
if (statusCode === 409) {
|
||||
notificationStore.error(getTrialAlreadyUsedMessage());
|
||||
} else if (statusCode === 429) {
|
||||
notificationStore.error(getTrialTryAgainLaterMessage());
|
||||
} else {
|
||||
notificationStore.error(
|
||||
getTrialStartErrorMessage(error instanceof Error ? error.message : undefined, {
|
||||
branded: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await runStartProTrialAction({
|
||||
branded: true,
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -12,16 +12,12 @@ import {
|
||||
licenseStatus,
|
||||
loadLicenseStatus,
|
||||
maxHistoryDays,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { calculateOptimalPoints } from '@/utils/downsample';
|
||||
import { setupCanvasDPR } from '@/utils/canvasRenderQueue';
|
||||
import { trackPaywallViewed, trackUpgradeClicked } from '@/utils/upgradeMetrics';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
import {
|
||||
HISTORY_CHART_RANGES,
|
||||
createHistoryChartGeometry,
|
||||
@@ -66,16 +62,11 @@ export function useHistoryChartState(props: HistoryChartProps, refs: HistoryChar
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = result.actionUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (err) {
|
||||
notificationStore.error(getTrialStartErrorMessage(err, { branded: true }));
|
||||
await runStartProTrialAction({
|
||||
branded: true,
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -27,17 +27,11 @@ import {
|
||||
hasFeature,
|
||||
licenseStatus,
|
||||
loadLicenseStatus,
|
||||
startProTrial,
|
||||
} from '@/stores/license';
|
||||
import { getCanonicalScopeResourceIds } from '@/utils/patrolFormat';
|
||||
import { buildPatrolInvestigationContextSummary } from './patrolInvestigationContextModel';
|
||||
import {
|
||||
getProTrialStartedMessage,
|
||||
getTrialAlreadyUsedMessage,
|
||||
getTrialStartErrorMessage,
|
||||
getTrialTryAgainLaterMessage,
|
||||
} from '@/utils/upgradePresentation';
|
||||
import { trackPaywallViewed } from '@/utils/upgradeMetrics';
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
interface ModelInfo {
|
||||
id: string;
|
||||
@@ -171,36 +165,26 @@ export function usePatrolIntelligenceState() {
|
||||
const autoFixLocked = createMemo(() => !hasFeature('ai_autofix'));
|
||||
|
||||
const canStartTrial = createMemo(() => {
|
||||
const state = licenseStatus()?.subscription_state;
|
||||
if (!state) return false;
|
||||
return state !== 'active' && state !== 'trial';
|
||||
const entitlements = licenseStatus();
|
||||
if (!entitlements) return false;
|
||||
if (
|
||||
entitlements.subscription_state === 'active' ||
|
||||
entitlements.subscription_state === 'trial'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return entitlements.trial_eligible !== false;
|
||||
});
|
||||
|
||||
async function handleStartTrial() {
|
||||
if (startingTrial()) return;
|
||||
setStartingTrial(true);
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = result.actionUrl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
notificationStore.success(getProTrialStartedMessage());
|
||||
} catch (err) {
|
||||
const statusCode = (err as { status?: number } | null)?.status;
|
||||
if (statusCode === 409) {
|
||||
notificationStore.error(getTrialAlreadyUsedMessage());
|
||||
} else if (statusCode === 429) {
|
||||
notificationStore.error(getTrialTryAgainLaterMessage());
|
||||
} else {
|
||||
notificationStore.error(
|
||||
getTrialStartErrorMessage(err instanceof Error ? err.message : undefined, {
|
||||
branded: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await runStartProTrialAction({
|
||||
branded: true,
|
||||
showSuccess: notificationStore.success,
|
||||
showError: notificationStore.error,
|
||||
});
|
||||
} finally {
|
||||
setStartingTrial(false);
|
||||
}
|
||||
|
||||
@@ -1564,7 +1564,8 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(relayOnboardingCardSource).not.toContain('RelayAPI.getStatus()');
|
||||
expect(relayOnboardingCardStateSource).toContain('RelayAPI.getStatus()');
|
||||
expect(relayOnboardingCardStateSource).toContain('loadLicenseStatus()');
|
||||
expect(relayOnboardingCardStateSource).toContain('startProTrial()');
|
||||
expect(relayOnboardingCardStateSource).toContain('runStartProTrialAction({');
|
||||
expect(relayOnboardingCardStateSource).not.toContain('startProTrial()');
|
||||
expect(organizationBillingPanelSource).not.toContain('normalizeOrgScope(getOrgID())');
|
||||
expect(organizationBillingPanelSource).not.toContain('createSignal(');
|
||||
expect(billingAdminPanelSource).not.toContain('createSignal(');
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const startProTrialMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@/stores/license', () => ({
|
||||
startProTrial: (...args: unknown[]) => startProTrialMock(...args),
|
||||
}));
|
||||
|
||||
import { runStartProTrialAction } from '@/utils/trialStartAction';
|
||||
|
||||
describe('runStartProTrialAction', () => {
|
||||
const showSuccess = vi.fn();
|
||||
const showError = vi.fn();
|
||||
const navigate = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
startProTrialMock.mockReset();
|
||||
showSuccess.mockReset();
|
||||
showError.mockReset();
|
||||
navigate.mockReset();
|
||||
});
|
||||
|
||||
it('reports activation success through the shared success message path', async () => {
|
||||
startProTrialMock.mockResolvedValue({ outcome: 'activated' });
|
||||
|
||||
await expect(
|
||||
runStartProTrialAction({
|
||||
showSuccess,
|
||||
showError,
|
||||
navigate,
|
||||
}),
|
||||
).resolves.toBe('activated');
|
||||
|
||||
expect(showSuccess).toHaveBeenCalledWith('Pro trial started');
|
||||
expect(showError).not.toHaveBeenCalled();
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('navigates through the hosted handoff when the backend requires signup', async () => {
|
||||
startProTrialMock.mockResolvedValue({
|
||||
outcome: 'redirect',
|
||||
actionUrl: 'https://cloud.pulserelay.pro/start-pro-trial',
|
||||
});
|
||||
|
||||
await expect(
|
||||
runStartProTrialAction({
|
||||
showSuccess,
|
||||
showError,
|
||||
navigate,
|
||||
}),
|
||||
).resolves.toBe('redirect');
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith('https://cloud.pulserelay.pro/start-pro-trial');
|
||||
expect(showSuccess).not.toHaveBeenCalled();
|
||||
expect(showError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves canonical denial messages instead of collapsing conflicts locally', async () => {
|
||||
startProTrialMock.mockRejectedValue({
|
||||
status: 409,
|
||||
code: 'trial_not_available',
|
||||
message: 'Trial cannot be started while a paid v5 license migration is pending',
|
||||
});
|
||||
|
||||
await expect(
|
||||
runStartProTrialAction({
|
||||
showSuccess,
|
||||
showError,
|
||||
navigate,
|
||||
}),
|
||||
).resolves.toBe('error');
|
||||
|
||||
expect(showError).toHaveBeenCalledWith(
|
||||
'Trial cannot be started while a paid v5 license migration is pending',
|
||||
);
|
||||
expect(showSuccess).not.toHaveBeenCalled();
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { startProTrial } from '@/stores/license';
|
||||
import { getProTrialStartedMessage, getTrialStartErrorMessage } from '@/utils/upgradePresentation';
|
||||
|
||||
export type StartProTrialActionOutcome = 'activated' | 'redirect' | 'error';
|
||||
|
||||
export interface RunStartProTrialActionOptions {
|
||||
branded?: boolean;
|
||||
successMessage?: string;
|
||||
showSuccess: (message: string) => void;
|
||||
showError: (message: string) => void;
|
||||
navigate?: (actionUrl: string) => void;
|
||||
}
|
||||
|
||||
function defaultNavigate(actionUrl: string) {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.location.href = actionUrl;
|
||||
}
|
||||
|
||||
export async function runStartProTrialAction(
|
||||
options: RunStartProTrialActionOptions,
|
||||
): Promise<StartProTrialActionOutcome> {
|
||||
const { branded = false, navigate = defaultNavigate, showError, showSuccess, successMessage } =
|
||||
options;
|
||||
|
||||
try {
|
||||
const result = await startProTrial();
|
||||
if (result?.outcome === 'redirect') {
|
||||
navigate(result.actionUrl);
|
||||
return 'redirect';
|
||||
}
|
||||
|
||||
showSuccess(successMessage ?? getProTrialStartedMessage());
|
||||
return 'activated';
|
||||
} catch (error) {
|
||||
showError(getTrialStartErrorMessage(error, { branded }));
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user