batchCooking/apps/web/src/features/app-update/version-compare.ts
Nicolas 5d4d6be2dc
Some checks failed
CI / lint (push) Successful in 6m25s
CI / intent-service-test (push) Successful in 5m1s
CI / build (push) Successful in 2m38s
CI / test (push) Failing after 11m54s
CI / e2e (push) Has been cancelled
feat(mobile): vérification automatique de mise à jour de version
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>
2026-09-06 10:52:35 +02:00

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;
}