Réutilise l'API Releases de Forgejo (publique, sans authentification) comme source de vérité pour "quelle est la dernière version" — zéro nouvel endpoint backend : useAppUpdateCheck compare App.getInfo().version (@capacitor/app, nouvelle dépendance) au tag_name de GET .../releases/latest, et pointe "Mettre à jour" vers l'URL stable de téléchargement (issue #65). Comparaison de version en logique pure (version-compare.ts), séparée du hook effectful — testée isolément via un test de composant Cypress (version-compare.cy.tsx, même patron que highlight-tech-steps.cy.tsx : aucun montage, juste describe/it/expect sur des fonctions pures). Natif uniquement (Capacitor.isNativePlatform()) : @capacitor/app et @capacitor/preferences restent en import dynamique dans le hook, jamais dans le bundle web (même patron que capacitor-token-store.ts) — vérifié dans le build (deux petits chunks séparés, pas dans le bundle principal). Vérification au démarrage à froid + à la reprise depuis l'arrière-plan (appStateChange), avec un throttle de 6h et une mémorisation de la version déjà rejetée dans @capacitor/preferences pour ne jamais re-harceler sur la même version. Bannière discrète (pas une Dialog modale), bouton "Mettre à jour" ouvrant le lien de téléchargement dans le navigateur système plutôt qu'un flux PackageInstaller in-app. Vérifié : pnpm lint, pnpm --filter web build, le test de composant (9/9), et layout/smoke/planning-page e2e (18/18, la bannière étant un no-op sur le web ne devait rien casser — confirmé). Closes #66 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|