import type { EnvironmentId, ServerConfig } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { APP_VERSION } from "./branding"; import { getLocalStorageItem, setLocalStorageItem } from "./hooks/useLocalStorage"; export interface VersionMismatch { readonly clientVersion: string; readonly serverVersion: string; readonly hint: string; } export const VERSION_MISMATCH_DISMISSALS_STORAGE_KEY = "t3code:version-mismatch-dismissals:v1"; const VersionMismatchDismissalsSchema = Schema.Struct({ keys: Schema.Array(Schema.String), }); type VersionMismatchDismissals = typeof VersionMismatchDismissalsSchema.Type; function normalizeVersion(version: string | null | undefined): string | null { const trimmed = version?.trim(); return trimmed && trimmed.length > 0 ? trimmed : null; } export function resolveVersionMismatch( serverVersion: string | null | undefined, ): VersionMismatch | null { const normalizedClientVersion = normalizeVersion(APP_VERSION); const normalizedServerVersion = normalizeVersion(serverVersion); if ( !normalizedClientVersion || !normalizedServerVersion || normalizedClientVersion === normalizedServerVersion ) { return null; } return { clientVersion: normalizedClientVersion, serverVersion: normalizedServerVersion, hint: "Version mismatch. Try syncing the client and server to the same T3 Code version.", }; } export function resolveServerConfigVersionMismatch( serverConfig: Pick | null | undefined, ): VersionMismatch | null { return resolveVersionMismatch(serverConfig?.environment.serverVersion); } export function buildVersionMismatchDismissalKey( environmentId: EnvironmentId, mismatch: Pick, ): string { return `${environmentId}:${mismatch.clientVersion}:${mismatch.serverVersion}`; } function readVersionMismatchDismissals(): VersionMismatchDismissals { try { return ( getLocalStorageItem( VERSION_MISMATCH_DISMISSALS_STORAGE_KEY, VersionMismatchDismissalsSchema, ) ?? { keys: [] } ); } catch { return { keys: [] }; } } function writeVersionMismatchDismissals(document: VersionMismatchDismissals): void { try { setLocalStorageItem( VERSION_MISMATCH_DISMISSALS_STORAGE_KEY, document, VersionMismatchDismissalsSchema, ); } catch { // Dismissal state is best-effort UI state; a storage failure should not block the banner. } } export function isVersionMismatchDismissed(dismissalKey: string | null | undefined): boolean { if (!dismissalKey) { return false; } return readVersionMismatchDismissals().keys.includes(dismissalKey); } export function dismissVersionMismatch(dismissalKey: string | null | undefined): void { if (!dismissalKey) { return; } const document = readVersionMismatchDismissals(); if (document.keys.includes(dismissalKey)) { return; } writeVersionMismatchDismissals({ keys: [...document.keys, dismissalKey], }); } export function appendVersionMismatchHint( message: string | null | undefined, mismatch: VersionMismatch | null | undefined, ): string | null { const normalizedMessage = normalizeVersion(message); if (!normalizedMessage) { return mismatch?.hint ?? null; } if (!mismatch) { return normalizedMessage; } return `${normalizedMessage} Hint: ${mismatch.hint}`; }