Split active use trial nudge owners

This commit is contained in:
rcourtman
2026-03-23 08:41:02 +00:00
parent 9420018e99
commit 76290a10fa
7 changed files with 363 additions and 108 deletions
@@ -214,6 +214,14 @@ signals, canvas draw lifecycle, and resize handling, and
math, hover target selection, tooltip time formatting, and density-cell
opacity rules. Future density-map work should extend those owners instead of
pushing canvas lifecycle or chart math back into the shared shell.
The shared active-use trial nudge now follows that same owner split.
`frontend-modern/src/components/shared/ActiveUseTrialNudge.tsx` stays the
render shell, `frontend-modern/src/components/shared/useActiveUseTrialNudgeState.ts`
owns first-seen persistence, snooze state, hourly age refresh, and trial-start
runtime, and `frontend-modern/src/components/shared/activeUseTrialNudgeModel.ts`
owns the eligibility policy, age threshold, and nudge copy/config. Future
active-use trial work should extend those owners instead of pushing storage
policy, timers, or commercial action flow back into the shared shell.
The shared dialog now follows that same owner split.
`frontend-modern/src/components/shared/Dialog.tsx` stays the render shell,
`frontend-modern/src/components/shared/useDialogState.ts` owns focus trap,
@@ -1,132 +1,41 @@
/**
* ActiveUseTrialNudge
*
* Proactive trial nudge shown after 7+ days of active use for free-tier users
* who haven't started a trial. Dismissible with 7-day snooze.
*/
import { Component, Show, createSignal, createMemo, onMount, onCleanup } from 'solid-js';
import { licenseStatus, startProTrial } from '@/stores/license';
import { isUpsellSnoozed, snoozeUpsell } from '@/utils/snooze';
import { notificationStore } from '@/stores/notifications';
import { Component, Show } from 'solid-js';
import {
getProTrialStartedMessage,
getTrialAlreadyUsedMessage,
getTrialStartErrorMessage,
getTrialTryAgainLaterMessage,
} from '@/utils/upgradePresentation';
const SNOOZE_KEY = 'pulse_active_use_nudge_snoozed';
const FIRST_SEEN_KEY = 'pulse_first_seen_ts';
const MIN_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
function getFirstSeenTimestamp(): number {
if (typeof window === 'undefined') return Date.now();
try {
const raw = window.localStorage.getItem(FIRST_SEEN_KEY);
if (raw) {
const ts = Number(raw);
if (Number.isFinite(ts) && ts > 0) return ts;
}
// First time: record now
const now = Date.now();
window.localStorage.setItem(FIRST_SEEN_KEY, String(now));
return now;
} catch {
return Date.now();
}
}
ACTIVE_USE_TRIAL_NUDGE_SNOOZE_LABEL,
ACTIVE_USE_TRIAL_NUDGE_STARTING_LABEL,
ACTIVE_USE_TRIAL_NUDGE_START_LABEL,
ACTIVE_USE_TRIAL_NUDGE_TITLE,
} from './activeUseTrialNudgeModel';
import { useActiveUseTrialNudgeState } from './useActiveUseTrialNudgeState';
export const ActiveUseTrialNudge: Component = () => {
const [snoozed, setSnoozed] = createSignal(isUpsellSnoozed(SNOOZE_KEY));
const [firstSeen, setFirstSeen] = createSignal(Date.now());
const [now, setNow] = createSignal(Date.now());
const [startingTrial, setStartingTrial] = createSignal(false);
onMount(() => {
setFirstSeen(getFirstSeenTimestamp());
// Re-evaluate once per hour so the nudge can appear if day 7 is crossed during a long session
const timer = setInterval(() => setNow(Date.now()), 60 * 60 * 1000);
onCleanup(() => clearInterval(timer));
});
const isOldEnough = createMemo(() => now() - firstSeen() >= MIN_AGE_MS);
const isFreeNoTrial = createMemo(() => {
const ent = licenseStatus();
if (!ent) return false;
// Only show for free tier users who are trial-eligible
return (
ent.tier === 'free' &&
ent.subscription_state !== 'trial' &&
ent.subscription_state !== 'active' &&
ent.trial_eligible !== false
);
});
const shouldShow = createMemo(() => isOldEnough() && isFreeNoTrial() && !snoozed());
const handleSnooze = () => {
snoozeUpsell(SNOOZE_KEY);
setSnoozed(true);
};
const handleStartTrial = async () => {
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,
}),
);
}
} finally {
setStartingTrial(false);
}
};
const state = useActiveUseTrialNudgeState();
return (
<Show when={shouldShow()}>
<Show when={state.shouldShow()}>
<div
class="mb-2 rounded-md border border-indigo-200 bg-indigo-50 dark:border-indigo-900 dark:bg-indigo-900/30 px-3 py-2 text-sm text-indigo-900 dark:text-indigo-100"
role="status"
aria-live="polite"
>
<div class="flex flex-wrap items-center justify-between gap-2">
<span class="font-medium">
Experience the full power of Pulse — start your free trial
</span>
<span class="font-medium">{ACTIVE_USE_TRIAL_NUDGE_TITLE}</span>
<div class="flex items-center gap-2">
<button
type="button"
class="text-xs font-semibold text-indigo-700 dark:text-indigo-300 hover:underline disabled:opacity-60"
disabled={startingTrial()}
onClick={handleStartTrial}
disabled={state.startingTrial()}
onClick={state.handleStartTrial}
>
{startingTrial() ? 'Starting...' : 'Start 14-day trial'}
{state.startingTrial()
? ACTIVE_USE_TRIAL_NUDGE_STARTING_LABEL
: ACTIVE_USE_TRIAL_NUDGE_START_LABEL}
</button>
<button
type="button"
class="text-xs opacity-70 hover:opacity-100"
onClick={handleSnooze}
onClick={state.handleSnooze}
>
Snooze 7d
{ACTIVE_USE_TRIAL_NUDGE_SNOOZE_LABEL}
</button>
</div>
</div>
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest';
import calloutCardSource from '@/components/shared/CalloutCard.tsx?raw';
import commandPaletteModalSource from '@/components/shared/CommandPaletteModal.tsx?raw';
import commandPaletteModelSource from '@/components/shared/commandPaletteModel.ts?raw';
import activeUseTrialNudgeSource from '@/components/shared/ActiveUseTrialNudge.tsx?raw';
import activeUseTrialNudgeModelSource from '@/components/shared/activeUseTrialNudgeModel.ts?raw';
import collapsibleSearchInputSource from '@/components/shared/CollapsibleSearchInput.tsx?raw';
import collapsibleSearchInputModelSource from '@/components/shared/collapsibleSearchInputModel.ts?raw';
import containerUpdateBadgeSource from '@/components/shared/ContainerUpdateBadge.tsx?raw';
@@ -46,6 +48,7 @@ import monitoredSystemLimitWarningBannerSource from '@/components/shared/Monitor
import selectionCardGroupSource from '@/components/shared/SelectionCardGroup.tsx?raw';
import tagBadgesSource from '@/components/shared/TagBadges.tsx?raw';
import commandPaletteStateSource from '@/components/shared/useCommandPaletteState.ts?raw';
import activeUseTrialNudgeStateSource from '@/components/shared/useActiveUseTrialNudgeState.ts?raw';
import collapsibleSearchInputStateSource from '@/components/shared/useCollapsibleSearchInputState.ts?raw';
import containerUpdateButtonStateSource from '@/components/shared/useContainerUpdateButtonState.ts?raw';
import densityMapStateSource from '@/components/shared/useDensityMapState.ts?raw';
@@ -147,6 +150,32 @@ describe('shared primitive guardrails', () => {
expect(commandPaletteModelSource).toContain('filterCommandPaletteCommands');
});
it('keeps active use trial nudge on shell, runtime, and model owners', () => {
expect(activeUseTrialNudgeSource).toContain('useActiveUseTrialNudgeState');
expect(activeUseTrialNudgeSource).toContain('ACTIVE_USE_TRIAL_NUDGE_TITLE');
expect(activeUseTrialNudgeSource).not.toContain('createSignal');
expect(activeUseTrialNudgeSource).not.toContain('createMemo');
expect(activeUseTrialNudgeSource).not.toContain('startProTrial');
expect(activeUseTrialNudgeSource).not.toContain('localStorage');
expect(activeUseTrialNudgeSource).not.toContain('setInterval');
expect(activeUseTrialNudgeStateSource).toContain(
'export function useActiveUseTrialNudgeState',
);
expect(activeUseTrialNudgeStateSource).toContain('createSignal');
expect(activeUseTrialNudgeStateSource).toContain('createMemo');
expect(activeUseTrialNudgeStateSource).toContain('window.localStorage');
expect(activeUseTrialNudgeStateSource).toContain('setInterval');
expect(activeUseTrialNudgeStateSource).toContain('startProTrial');
expect(activeUseTrialNudgeStateSource).toContain('snoozeUpsell');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY');
expect(activeUseTrialNudgeModelSource).toContain('isActiveUseTrialNudgeEligible');
expect(activeUseTrialNudgeModelSource).toContain('isActiveUseTrialNudgeOldEnough');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_TITLE');
});
it('routes settings info callouts through CalloutCard', () => {
expect(calloutCardSource).toContain(
"type CalloutTone = 'danger' | 'info' | 'success' | 'warning'",
@@ -0,0 +1,158 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import activeUseTrialNudgeSource from '@/components/shared/ActiveUseTrialNudge.tsx?raw';
import activeUseTrialNudgeModelSource from '@/components/shared/activeUseTrialNudgeModel.ts?raw';
import activeUseTrialNudgeStateSource from '@/components/shared/useActiveUseTrialNudgeState.ts?raw';
import {
ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY,
ACTIVE_USE_TRIAL_NUDGE_MIN_AGE_MS,
ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY,
} from '@/components/shared/activeUseTrialNudgeModel';
const {
licenseStatusMock,
startProTrialMock,
showSuccessMock,
showErrorMock,
isUpsellSnoozedMock,
snoozeUpsellMock,
} = vi.hoisted(() => ({
licenseStatusMock: vi.fn(),
startProTrialMock: vi.fn(),
showSuccessMock: vi.fn(),
showErrorMock: vi.fn(),
isUpsellSnoozedMock: vi.fn(),
snoozeUpsellMock: vi.fn(),
}));
vi.mock('@/stores/license', () => ({
licenseStatus: (...args: unknown[]) => licenseStatusMock(...args),
startProTrial: (...args: unknown[]) => startProTrialMock(...args),
}));
vi.mock('@/stores/notifications', () => ({
notificationStore: {
success: (...args: unknown[]) => showSuccessMock(...args),
error: (...args: unknown[]) => showErrorMock(...args),
},
}));
vi.mock('@/utils/snooze', () => ({
isUpsellSnoozed: (...args: unknown[]) => isUpsellSnoozedMock(...args),
snoozeUpsell: (...args: unknown[]) => snoozeUpsellMock(...args),
}));
vi.mock('@/utils/upgradePresentation', () => ({
getProTrialStartedMessage: () => 'Pro trial started',
getTrialAlreadyUsedMessage: () => 'Trial already used',
getTrialStartErrorMessage: () => 'Trial start failed',
getTrialTryAgainLaterMessage: () => 'Try again later',
}));
import { ActiveUseTrialNudge } from '@/components/shared/ActiveUseTrialNudge';
function setEligibleFreeLicense() {
licenseStatusMock.mockReturnValue({
tier: 'free',
subscription_state: 'expired',
trial_eligible: true,
});
}
describe('ActiveUseTrialNudge', () => {
beforeEach(() => {
localStorage.clear();
licenseStatusMock.mockReset();
startProTrialMock.mockReset();
showSuccessMock.mockReset();
showErrorMock.mockReset();
isUpsellSnoozedMock.mockReset();
snoozeUpsellMock.mockReset();
isUpsellSnoozedMock.mockReturnValue(false);
});
afterEach(() => {
cleanup();
});
it('keeps active use trial nudge on shell, runtime, and model owners', () => {
expect(activeUseTrialNudgeSource).toContain('useActiveUseTrialNudgeState');
expect(activeUseTrialNudgeSource).toContain('ACTIVE_USE_TRIAL_NUDGE_TITLE');
expect(activeUseTrialNudgeSource).not.toContain('createSignal');
expect(activeUseTrialNudgeSource).not.toContain('createMemo');
expect(activeUseTrialNudgeSource).not.toContain('startProTrial');
expect(activeUseTrialNudgeSource).not.toContain('localStorage');
expect(activeUseTrialNudgeSource).not.toContain('setInterval');
expect(activeUseTrialNudgeStateSource).toContain('export function useActiveUseTrialNudgeState');
expect(activeUseTrialNudgeStateSource).toContain('createSignal');
expect(activeUseTrialNudgeStateSource).toContain('createMemo');
expect(activeUseTrialNudgeStateSource).toContain('window.localStorage');
expect(activeUseTrialNudgeStateSource).toContain('setInterval');
expect(activeUseTrialNudgeStateSource).toContain('startProTrial');
expect(activeUseTrialNudgeStateSource).toContain('snoozeUpsell');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY');
expect(activeUseTrialNudgeModelSource).toContain('isActiveUseTrialNudgeEligible');
expect(activeUseTrialNudgeModelSource).toContain('isActiveUseTrialNudgeOldEnough');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_TITLE');
});
it('renders for eligible free users after the minimum active age', async () => {
setEligibleFreeLicense();
localStorage.setItem(
ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY,
String(Date.now() - ACTIVE_USE_TRIAL_NUDGE_MIN_AGE_MS - 1),
);
render(() => <ActiveUseTrialNudge />);
expect(await screen.findByRole('status')).toBeInTheDocument();
expect(screen.getByText('Experience the full power of Pulse — start your free trial')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Start 14-day trial' })).toBeInTheDocument();
});
it('does not render before the active age threshold is crossed', () => {
setEligibleFreeLicense();
render(() => <ActiveUseTrialNudge />);
expect(screen.queryByRole('status')).toBeNull();
expect(localStorage.getItem(ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY)).not.toBeNull();
});
it('snoozes and hides the nudge', async () => {
setEligibleFreeLicense();
localStorage.setItem(
ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY,
String(Date.now() - ACTIVE_USE_TRIAL_NUDGE_MIN_AGE_MS - 1),
);
render(() => <ActiveUseTrialNudge />);
fireEvent.click(await screen.findByRole('button', { name: 'Snooze 7d' }));
expect(snoozeUpsellMock).toHaveBeenCalledWith(ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY);
await waitFor(() => {
expect(screen.queryByRole('status')).toBeNull();
});
});
it('starts a trial successfully and shows a success notification', async () => {
setEligibleFreeLicense();
startProTrialMock.mockResolvedValue({ outcome: 'activated' });
localStorage.setItem(
ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY,
String(Date.now() - ACTIVE_USE_TRIAL_NUDGE_MIN_AGE_MS - 1),
);
render(() => <ActiveUseTrialNudge />);
fireEvent.click(await screen.findByRole('button', { name: 'Start 14-day trial' }));
await waitFor(() => {
expect(startProTrialMock).toHaveBeenCalledTimes(1);
});
expect(showSuccessMock).toHaveBeenCalledWith('Pro trial started');
});
});
@@ -0,0 +1,29 @@
import type { LicenseEntitlements } from '@/api/license';
export const ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY = 'pulse_active_use_nudge_snoozed';
export const ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY = 'pulse_first_seen_ts';
export const ACTIVE_USE_TRIAL_NUDGE_MIN_AGE_MS = 7 * 24 * 60 * 60 * 1000;
export const ACTIVE_USE_TRIAL_NUDGE_REFRESH_MS = 60 * 60 * 1000;
export const ACTIVE_USE_TRIAL_NUDGE_TITLE =
'Experience the full power of Pulse — start your free trial';
export const ACTIVE_USE_TRIAL_NUDGE_START_LABEL = 'Start 14-day trial';
export const ACTIVE_USE_TRIAL_NUDGE_STARTING_LABEL = 'Starting...';
export const ACTIVE_USE_TRIAL_NUDGE_SNOOZE_LABEL = 'Snooze 7d';
export function isActiveUseTrialNudgeEligible(
entitlements: LicenseEntitlements | null | undefined,
): boolean {
if (!entitlements) return false;
return (
entitlements.tier === 'free' &&
entitlements.subscription_state !== 'trial' &&
entitlements.subscription_state !== 'active' &&
entitlements.trial_eligible !== false
);
}
export function isActiveUseTrialNudgeOldEnough(now: number, firstSeen: number): boolean {
return now - firstSeen >= ACTIVE_USE_TRIAL_NUDGE_MIN_AGE_MS;
}
@@ -0,0 +1,101 @@
import { createMemo, createSignal, onCleanup, onMount } from 'solid-js';
import { licenseStatus, startProTrial } from '@/stores/license';
import { notificationStore } from '@/stores/notifications';
import { isUpsellSnoozed, snoozeUpsell } from '@/utils/snooze';
import {
getProTrialStartedMessage,
getTrialAlreadyUsedMessage,
getTrialStartErrorMessage,
getTrialTryAgainLaterMessage,
} from '@/utils/upgradePresentation';
import {
ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY,
ACTIVE_USE_TRIAL_NUDGE_REFRESH_MS,
ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY,
isActiveUseTrialNudgeEligible,
isActiveUseTrialNudgeOldEnough,
} from './activeUseTrialNudgeModel';
function getActiveUseTrialNudgeFirstSeenTimestamp(): number {
if (typeof window === 'undefined') return Date.now();
try {
const raw = window.localStorage.getItem(ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY);
if (raw) {
const timestamp = Number(raw);
if (Number.isFinite(timestamp) && timestamp > 0) return timestamp;
}
const now = Date.now();
window.localStorage.setItem(ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY, String(now));
return now;
} catch {
return Date.now();
}
}
export function useActiveUseTrialNudgeState() {
const [snoozed, setSnoozed] = createSignal(
isUpsellSnoozed(ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY),
);
const [firstSeen, setFirstSeen] = createSignal(Date.now());
const [now, setNow] = createSignal(Date.now());
const [startingTrial, setStartingTrial] = createSignal(false);
onMount(() => {
setFirstSeen(getActiveUseTrialNudgeFirstSeenTimestamp());
const timer = window.setInterval(() => setNow(Date.now()), ACTIVE_USE_TRIAL_NUDGE_REFRESH_MS);
onCleanup(() => window.clearInterval(timer));
});
const shouldShow = createMemo(() => {
return (
isActiveUseTrialNudgeOldEnough(now(), firstSeen()) &&
isActiveUseTrialNudgeEligible(licenseStatus()) &&
!snoozed()
);
});
const handleSnooze = () => {
snoozeUpsell(ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY);
setSnoozed(true);
};
const handleStartTrial = async () => {
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) {
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,
}),
);
}
} finally {
setStartingTrial(false);
}
};
return {
handleSnooze,
handleStartTrial,
shouldShow,
startingTrial,
};
}
@@ -10,6 +10,8 @@ import alertTargetTypesSource from '@/utils/alertTargetTypes.ts?raw';
import resourceBadgesSource from '@/components/Infrastructure/resourceBadges.ts?raw';
import commandPaletteModalSource from '@/components/shared/CommandPaletteModal.tsx?raw';
import commandPaletteModelSource from '@/components/shared/commandPaletteModel.ts?raw';
import activeUseTrialNudgeSource from '@/components/shared/ActiveUseTrialNudge.tsx?raw';
import activeUseTrialNudgeModelSource from '@/components/shared/activeUseTrialNudgeModel.ts?raw';
import collapsibleSearchInputSource from '@/components/shared/CollapsibleSearchInput.tsx?raw';
import collapsibleSearchInputModelSource from '@/components/shared/collapsibleSearchInputModel.ts?raw';
import containerUpdateBadgeSource from '@/components/shared/ContainerUpdateBadge.tsx?raw';
@@ -47,6 +49,7 @@ import interactiveSparklineModelSource from '@/components/shared/interactiveSpar
import infrastructureSelectorModelSource from '@/components/shared/infrastructureSelectorModel.ts?raw';
import sharedInfrastructureSummaryTableModelSource from '@/components/shared/infrastructureSummaryTableModel.ts?raw';
import commandPaletteStateSource from '@/components/shared/useCommandPaletteState.ts?raw';
import activeUseTrialNudgeStateSource from '@/components/shared/useActiveUseTrialNudgeState.ts?raw';
import collapsibleSearchInputStateSource from '@/components/shared/useCollapsibleSearchInputState.ts?raw';
import containerUpdateButtonStateSource from '@/components/shared/useContainerUpdateButtonState.ts?raw';
import dialogStateSource from '@/components/shared/useDialogState.ts?raw';
@@ -2709,6 +2712,24 @@ describe('frontend resource type boundaries', () => {
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverPositionClass');
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerVariant');
expect(searchTipsPopoverModelSource).toContain('shouldSearchTipsPopoverOpenOnHover');
expect(activeUseTrialNudgeSource).toContain('useActiveUseTrialNudgeState');
expect(activeUseTrialNudgeSource).toContain('ACTIVE_USE_TRIAL_NUDGE_TITLE');
expect(activeUseTrialNudgeSource).not.toContain('createSignal');
expect(activeUseTrialNudgeSource).not.toContain('createMemo');
expect(activeUseTrialNudgeSource).not.toContain('startProTrial');
expect(activeUseTrialNudgeSource).not.toContain('localStorage');
expect(activeUseTrialNudgeSource).not.toContain('setInterval');
expect(activeUseTrialNudgeStateSource).toContain('createSignal');
expect(activeUseTrialNudgeStateSource).toContain('createMemo');
expect(activeUseTrialNudgeStateSource).toContain('window.localStorage');
expect(activeUseTrialNudgeStateSource).toContain('setInterval');
expect(activeUseTrialNudgeStateSource).toContain('startProTrial');
expect(activeUseTrialNudgeStateSource).toContain('snoozeUpsell');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_SNOOZE_KEY');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_FIRST_SEEN_KEY');
expect(activeUseTrialNudgeModelSource).toContain('isActiveUseTrialNudgeEligible');
expect(activeUseTrialNudgeModelSource).toContain('isActiveUseTrialNudgeOldEnough');
expect(activeUseTrialNudgeModelSource).toContain('ACTIVE_USE_TRIAL_NUDGE_TITLE');
expect(whatsNewModalSource).toContain('useWhatsNewModalState');
expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS');
expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal');