/** * Pure version-comparison logic for the mobile update check (issue #66) — * no Capacitor/network/DOM dependency, kept separate from * `useAppUpdateCheck.ts` so it's testable in isolation (same * pure-logic/effectful-hook split this repo uses everywhere else, e.g. * `ingredient-matcher.ts`'s `matchIngredientName` vs `loadIngredientCatalog`). * * Release tags in this repo are always `vX.Y.Z` (see `.github/workflows/release.yml`'s * `on: push: tags: ["v*.*.*"]`) — a hand-rolled comparison for exactly that * shape is simpler than pulling in a semver library for three integers. */ /** One `vX.Y.Z` tag parsed into its three integer components. */ export interface ParsedVersion { major: number; minor: number; patch: number; } /** * Parses a `vX.Y.Z` (or bare `X.Y.Z`) string into its components — `null` * if it doesn't match that exact shape (a malformed tag, an empty string, a * pre-release suffix this app doesn't publish). Callers treat `null` as * "can't compare, assume no update" rather than throwing — a version check * is never allowed to be the reason the app breaks. */ export function parseVersion(raw: string): ParsedVersion | null { const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(raw.trim()); if (!match) return null; const [, major, minor, patch] = match; return { major: Number(major), minor: Number(minor), patch: Number(patch) }; } /** * `true` when `latest` is a genuinely newer version than `current` — * `false` for equal, older, or unparseable input on either side (fails * closed: "don't nag" is always the safe default for a background check). */ export function isNewerVersion(current: string, latest: string): boolean { const currentParsed = parseVersion(current); const latestParsed = parseVersion(latest); if (!currentParsed || !latestParsed) return false; if (latestParsed.major !== currentParsed.major) { return latestParsed.major > currentParsed.major; } if (latestParsed.minor !== currentParsed.minor) { return latestParsed.minor > currentParsed.minor; } return latestParsed.patch > currentParsed.patch; }