Normalize Relay no-token status copy

This commit is contained in:
rcourtman
2026-04-28 19:37:50 +01:00
parent aa517d6221
commit f0650e8d01
6 changed files with 104 additions and 3 deletions
@@ -251,6 +251,10 @@ Community limit enforcement.
Public demo and other read-only presentation policy states must suppress
relay setup and upsell onboarding instead of inviting pairing or commercial
action from a governed non-manageable surface.
Relay settings must also translate internal registration-token failures
into an operator-actionable activation-required state. Customer-facing
Relay settings must not render raw `register:` or license-token-provider
diagnostics from the relay client as the primary status message.
19. Add or change cloud plan presentation through `frontend-modern/src/pages/CloudPricing.tsx`
That same presentation boundary also owns truthful customer-entry copy for
hosted Cloud pricing and signup. Cloud CTA labels, setup steps, and
@@ -102,8 +102,8 @@ export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) =>
</Show>
</div>
</div>
<Show when={state.status()?.last_error}>
<div class={RELAY_LAST_ERROR_CLASS}>{state.status()!.last_error}</div>
<Show when={state.statusErrorMessage()}>
{(message) => <div class={RELAY_LAST_ERROR_CLASS}>{message()}</div>}
</Show>
</Card>
@@ -241,6 +241,29 @@ describe('RelaySettingsPanel runtime', () => {
expect(getQRPayloadMock).not.toHaveBeenCalled();
});
it('shows activation-required copy instead of raw missing-token relay errors', async () => {
getRelayStatusMock.mockResolvedValueOnce({
connected: false,
instance_id: 'instance-local',
active_channels: 0,
last_error: 'register: no license token available',
});
render(() => <RelaySettingsPanel canManage />);
await waitFor(() => {
expect(screen.getByText('Activation required')).toBeInTheDocument();
});
expect(
screen.getByText(
'Remote Access is enabled, but this instance does not have an active Relay token. Activate Relay or turn Remote Access off before pairing mobile clients.',
),
).toBeInTheDocument();
expect(screen.queryByText('register: no license token available')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Pair New Device' })).not.toBeInTheDocument();
});
it('deletes the minted pairing token when onboarding payload generation fails', async () => {
getQRPayloadMock.mockRejectedValueOnce(new Error('missing auth token'));
@@ -9,7 +9,10 @@ import { RelayAPI, type RelayConfig, type RelayStatus } from '@/api/relay';
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 {
getRelayConnectionPresentation,
getRelayStatusErrorMessage,
} from '@/utils/relayPresentation';
import QRCode from 'qrcode';
export interface RelaySettingsPanelProps {
@@ -35,6 +38,7 @@ export function useRelaySettingsPanelState(props: RelaySettingsPanelProps) {
const connectionPresentation = createMemo(() =>
getRelayConnectionPresentation(config(), status()),
);
const statusErrorMessage = createMemo(() => getRelayStatusErrorMessage(status()));
const canShowPairing = createMemo(() => Boolean(config()?.enabled && status()?.connected));
createEffect((wasPaywallVisible: boolean) => {
@@ -267,6 +271,7 @@ export function useRelaySettingsPanelState(props: RelaySettingsPanelProps) {
showUpgradePrompts,
showPairing,
status,
statusErrorMessage,
upgradeDestination,
};
}
@@ -1,7 +1,10 @@
import { describe, expect, it } from 'vitest';
import {
getRelayStatusErrorMessage,
getRelayConnectionPresentation,
getRelayDiagnosticClass,
RELAY_ACTIVATION_REQUIRED_LABEL,
RELAY_ACTIVATION_REQUIRED_MESSAGE,
RELAY_CODE_BLOCK_CLASS,
RELAY_ENABLE_HELP_TEXT,
RELAY_INFO_MESSAGE_CLASS,
@@ -49,6 +52,42 @@ describe('relayPresentation', () => {
});
});
it('returns activation-required presentation for missing Relay token errors', () => {
expect(
getRelayConnectionPresentation(
{ enabled: true } as never,
{
connected: false,
active_channels: 0,
last_error: 'register: no license token available',
},
),
).toEqual({
variant: 'danger',
label: RELAY_ACTIVATION_REQUIRED_LABEL,
pulse: false,
});
});
it('translates missing Relay token errors while preserving other diagnostics', () => {
expect(
getRelayStatusErrorMessage({
connected: false,
active_channels: 0,
last_error: 'register: no license token available',
}),
).toBe(RELAY_ACTIVATION_REQUIRED_MESSAGE);
expect(
getRelayStatusErrorMessage({
connected: false,
active_channels: 0,
last_error: 'relay handshake failed',
}),
).toBe('relay handshake failed');
expect(getRelayStatusErrorMessage(null)).toBeNull();
});
it('centralizes relay action and diagnostics styling', () => {
expect(RELAY_READONLY_NOTICE_CLASS).toContain('border-blue-200');
expect(RELAY_PRIMARY_BUTTON_CLASS).toContain('bg-blue-600');
@@ -71,6 +110,8 @@ describe('relayPresentation', () => {
expect(RELAY_PAIRING_AVAILABILITY_TITLE).toBe('Pair Pulse Mobile through Relay');
expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('QR code or deep link');
expect(RELAY_ENABLE_HELP_TEXT).toContain('Pulse Mobile pairing');
expect(RELAY_ACTIVATION_REQUIRED_LABEL).toBe('Activation required');
expect(RELAY_ACTIVATION_REQUIRED_MESSAGE).toContain('active Relay token');
});
it('does not retain retired Relay price or trial-era onboarding copy', () => {
@@ -7,6 +7,8 @@ export interface RelayConnectionPresentation {
pulse: boolean;
}
const RELAY_MISSING_TOKEN_ERROR = /\b(?:no license token available|license token provider not configured)\b/i;
export const RELAY_READONLY_NOTICE_CLASS =
'border border-blue-200 text-xs text-blue-800 dark:border-blue-800 dark:text-blue-200';
export const RELAY_PRIMARY_BUTTON_CLASS =
@@ -35,6 +37,9 @@ export const RELAY_PAIRING_AVAILABILITY_MESSAGE =
'Supported Pulse Mobile clients connect to this Pulse instance with a QR code or deep link over end-to-end encrypted relay connectivity.';
export const RELAY_ENABLE_HELP_TEXT =
'Connect this Pulse instance to the relay server for secure remote access and Pulse Mobile pairing.';
export const RELAY_ACTIVATION_REQUIRED_LABEL = 'Activation required';
export const RELAY_ACTIVATION_REQUIRED_MESSAGE =
'Remote Access is enabled, but this instance does not have an active Relay token. Activate Relay or turn Remote Access off before pairing mobile clients.';
export function getRelayDiagnosticClass(severity: 'warning' | 'error'): string {
return severity === 'error'
@@ -62,9 +67,32 @@ export function getRelayConnectionPresentation(
};
}
if (isRelayMissingTokenError(status?.last_error)) {
return {
variant: 'danger',
label: RELAY_ACTIVATION_REQUIRED_LABEL,
pulse: false,
};
}
return {
variant: 'danger',
label: 'Disconnected',
pulse: false,
};
}
export function getRelayStatusErrorMessage(status?: RelayStatus | null): string | null {
const error = status?.last_error?.trim();
if (!error) {
return null;
}
if (isRelayMissingTokenError(error)) {
return RELAY_ACTIVATION_REQUIRED_MESSAGE;
}
return error;
}
function isRelayMissingTokenError(error?: string | null): boolean {
return RELAY_MISSING_TOKEN_ERROR.test(error ?? '');
}