mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Normalize Relay no-token status copy
This commit is contained in:
@@ -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 ?? '');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user