feat: gate auto-updates behind license entitlement (#48)
Both code paths that touch the updater now consult `check_entitlement` from the Rust entitlements module before calling `check()`: - `useUpdater.ts` adds a `notEntitled` status; on Free, the check short-circuits and the Settings page displays an upgrade hint instead of fetching update metadata. - `ErrorPage.tsx` (recovery screen) does the same so the error path matches the main path; users on Free no longer see network errors when the updater would have run. The gate name (`auto-update`) is the same string consumed by `commands/entitlements.rs::FEATURE_TIERS`, so changing which tier unlocks updates is a one-line edit in that file. Bilingual i18n keys for the new messages are added to both `fr.json` and `en.json`. CHANGELOG entries in both languages.
This commit is contained in:
parent
59cefe8435
commit
6d67ab8935
7 changed files with 49 additions and 3 deletions
|
|
@ -5,6 +5,9 @@
|
||||||
### Ajouté
|
### Ajouté
|
||||||
- CI : nouveau workflow `check.yml` qui exécute `cargo check`/`cargo test` et le build frontend sur chaque push de branche et PR, détectant les erreurs avant le merge plutôt qu'au moment de la release (#60)
|
- CI : nouveau workflow `check.yml` qui exécute `cargo check`/`cargo test` et le build frontend sur chaque push de branche et PR, détectant les erreurs avant le merge plutôt qu'au moment de la release (#60)
|
||||||
|
|
||||||
|
### Modifié
|
||||||
|
- Les mises à jour automatiques sont maintenant réservées à l'édition Base ; l'édition Gratuite affiche un message invitant à activer une licence (#48)
|
||||||
|
|
||||||
## [0.6.7] - 2026-03-29
|
## [0.6.7] - 2026-03-29
|
||||||
|
|
||||||
### Modifié
|
### Modifié
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
### Added
|
### Added
|
||||||
- CI: new `check.yml` workflow runs `cargo check`/`cargo test` and the frontend build on every branch push and PR, catching errors before merge instead of waiting for the release tag (#60)
|
- CI: new `check.yml` workflow runs `cargo check`/`cargo test` and the frontend build on every branch push and PR, catching errors before merge instead of waiting for the release tag (#60)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Automatic updates are now gated behind the Base edition entitlement; the Free edition shows an upgrade hint instead of fetching updates (#48)
|
||||||
|
|
||||||
## [0.6.7] - 2026-03-29
|
## [0.6.7] - 2026-03-29
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AlertTriangle, ChevronDown, ChevronUp, RefreshCw, Download, Mail, Bug } from "lucide-react";
|
import { AlertTriangle, ChevronDown, ChevronUp, RefreshCw, Download, Mail, Bug } from "lucide-react";
|
||||||
import { check } from "@tauri-apps/plugin-updater";
|
import { check } from "@tauri-apps/plugin-updater";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
interface ErrorPageProps {
|
interface ErrorPageProps {
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|
@ -10,7 +11,7 @@ interface ErrorPageProps {
|
||||||
export default function ErrorPage({ error }: ErrorPageProps) {
|
export default function ErrorPage({ error }: ErrorPageProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showDetails, setShowDetails] = useState(false);
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
const [updateStatus, setUpdateStatus] = useState<"idle" | "checking" | "available" | "upToDate" | "error">("idle");
|
const [updateStatus, setUpdateStatus] = useState<"idle" | "checking" | "available" | "upToDate" | "notEntitled" | "error">("idle");
|
||||||
const [updateVersion, setUpdateVersion] = useState<string | null>(null);
|
const [updateVersion, setUpdateVersion] = useState<string | null>(null);
|
||||||
const [updateError, setUpdateError] = useState<string | null>(null);
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
|
@ -18,6 +19,13 @@ export default function ErrorPage({ error }: ErrorPageProps) {
|
||||||
setUpdateStatus("checking");
|
setUpdateStatus("checking");
|
||||||
setUpdateError(null);
|
setUpdateError(null);
|
||||||
try {
|
try {
|
||||||
|
const allowed = await invoke<boolean>("check_entitlement", {
|
||||||
|
feature: "auto-update",
|
||||||
|
});
|
||||||
|
if (!allowed) {
|
||||||
|
setUpdateStatus("notEntitled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const update = await check();
|
const update = await check();
|
||||||
if (update) {
|
if (update) {
|
||||||
setUpdateStatus("available");
|
setUpdateStatus("available");
|
||||||
|
|
@ -89,6 +97,11 @@ export default function ErrorPage({ error }: ErrorPageProps) {
|
||||||
{t("error.upToDate")}
|
{t("error.upToDate")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{updateStatus === "notEntitled" && (
|
||||||
|
<p className="text-sm text-[var(--muted-foreground)]">
|
||||||
|
{t("error.updateNotEntitled")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{updateStatus === "error" && updateError && (
|
{updateStatus === "error" && updateError && (
|
||||||
<p className="text-sm text-[var(--destructive)]">{updateError}</p>
|
<p className="text-sm text-[var(--destructive)]">{updateError}</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useReducer, useCallback, useRef } from "react";
|
import { useReducer, useCallback, useRef } from "react";
|
||||||
import { check, type Update } from "@tauri-apps/plugin-updater";
|
import { check, type Update } from "@tauri-apps/plugin-updater";
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
type UpdateStatus =
|
type UpdateStatus =
|
||||||
| "idle"
|
| "idle"
|
||||||
|
|
@ -10,6 +11,7 @@ type UpdateStatus =
|
||||||
| "downloading"
|
| "downloading"
|
||||||
| "readyToInstall"
|
| "readyToInstall"
|
||||||
| "installing"
|
| "installing"
|
||||||
|
| "notEntitled"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
interface UpdaterState {
|
interface UpdaterState {
|
||||||
|
|
@ -29,6 +31,7 @@ type UpdaterAction =
|
||||||
| { type: "DOWNLOAD_PROGRESS"; downloaded: number; contentLength: number | null }
|
| { type: "DOWNLOAD_PROGRESS"; downloaded: number; contentLength: number | null }
|
||||||
| { type: "READY_TO_INSTALL" }
|
| { type: "READY_TO_INSTALL" }
|
||||||
| { type: "INSTALLING" }
|
| { type: "INSTALLING" }
|
||||||
|
| { type: "NOT_ENTITLED" }
|
||||||
| { type: "ERROR"; error: string };
|
| { type: "ERROR"; error: string };
|
||||||
|
|
||||||
const initialState: UpdaterState = {
|
const initialState: UpdaterState = {
|
||||||
|
|
@ -56,6 +59,8 @@ function reducer(state: UpdaterState, action: UpdaterAction): UpdaterState {
|
||||||
return { ...state, status: "readyToInstall", error: null };
|
return { ...state, status: "readyToInstall", error: null };
|
||||||
case "INSTALLING":
|
case "INSTALLING":
|
||||||
return { ...state, status: "installing", error: null };
|
return { ...state, status: "installing", error: null };
|
||||||
|
case "NOT_ENTITLED":
|
||||||
|
return { ...state, status: "notEntitled", error: null };
|
||||||
case "ERROR":
|
case "ERROR":
|
||||||
return { ...state, status: "error", error: action.error };
|
return { ...state, status: "error", error: action.error };
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +73,16 @@ export function useUpdater() {
|
||||||
const checkForUpdate = useCallback(async () => {
|
const checkForUpdate = useCallback(async () => {
|
||||||
dispatch({ type: "CHECK_START" });
|
dispatch({ type: "CHECK_START" });
|
||||||
try {
|
try {
|
||||||
|
// Auto-updates are gated behind the entitlements module (Issue #46/#48).
|
||||||
|
// The check is centralized server-side via `check_entitlement` so the
|
||||||
|
// tier→feature mapping lives in one place.
|
||||||
|
const allowed = await invoke<boolean>("check_entitlement", {
|
||||||
|
feature: "auto-update",
|
||||||
|
});
|
||||||
|
if (!allowed) {
|
||||||
|
dispatch({ type: "NOT_ENTITLED" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const update = await check();
|
const update = await check();
|
||||||
if (update) {
|
if (update) {
|
||||||
updateRef.current = update;
|
updateRef.current = update;
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,8 @@
|
||||||
"installing": "Installing...",
|
"installing": "Installing...",
|
||||||
"error": "Update failed",
|
"error": "Update failed",
|
||||||
"retryButton": "Retry",
|
"retryButton": "Retry",
|
||||||
"releaseNotes": "What's New"
|
"releaseNotes": "What's New",
|
||||||
|
"notEntitled": "Automatic updates are available with the Base edition. Activate a license to enable them."
|
||||||
},
|
},
|
||||||
"dataManagement": {
|
"dataManagement": {
|
||||||
"title": "Data Management",
|
"title": "Data Management",
|
||||||
|
|
@ -827,6 +828,7 @@
|
||||||
"checkUpdate": "Check for updates",
|
"checkUpdate": "Check for updates",
|
||||||
"updateAvailable": "Update available: v{{version}}",
|
"updateAvailable": "Update available: v{{version}}",
|
||||||
"upToDate": "The application is up to date",
|
"upToDate": "The application is up to date",
|
||||||
|
"updateNotEntitled": "Automatic updates are available with the Base edition.",
|
||||||
"contactUs": "Contact us",
|
"contactUs": "Contact us",
|
||||||
"contactEmail": "Send an email to",
|
"contactEmail": "Send an email to",
|
||||||
"reportIssue": "Report an issue"
|
"reportIssue": "Report an issue"
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,8 @@
|
||||||
"installing": "Installation en cours...",
|
"installing": "Installation en cours...",
|
||||||
"error": "Erreur lors de la mise à jour",
|
"error": "Erreur lors de la mise à jour",
|
||||||
"retryButton": "Réessayer",
|
"retryButton": "Réessayer",
|
||||||
"releaseNotes": "Nouveautés"
|
"releaseNotes": "Nouveautés",
|
||||||
|
"notEntitled": "Les mises à jour automatiques sont disponibles avec l'édition Base. Activez une licence pour les utiliser."
|
||||||
},
|
},
|
||||||
"dataManagement": {
|
"dataManagement": {
|
||||||
"title": "Gestion des données",
|
"title": "Gestion des données",
|
||||||
|
|
@ -827,6 +828,7 @@
|
||||||
"checkUpdate": "Vérifier les mises à jour",
|
"checkUpdate": "Vérifier les mises à jour",
|
||||||
"updateAvailable": "Mise à jour disponible : v{{version}}",
|
"updateAvailable": "Mise à jour disponible : v{{version}}",
|
||||||
"upToDate": "L'application est à jour",
|
"upToDate": "L'application est à jour",
|
||||||
|
"updateNotEntitled": "Les mises à jour automatiques sont disponibles avec l'édition Base.",
|
||||||
"contactUs": "Nous contacter",
|
"contactUs": "Nous contacter",
|
||||||
"contactEmail": "Envoyez un email à",
|
"contactEmail": "Envoyez un email à",
|
||||||
"reportIssue": "Signaler un problème"
|
"reportIssue": "Signaler un problème"
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,14 @@ export default function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* not entitled (free edition) */}
|
||||||
|
{state.status === "notEntitled" && (
|
||||||
|
<div className="flex items-start gap-2 text-sm text-[var(--muted-foreground)]">
|
||||||
|
<AlertCircle size={16} className="mt-0.5 shrink-0" />
|
||||||
|
<p>{t("settings.updates.notEntitled")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* up to date */}
|
{/* up to date */}
|
||||||
{state.status === "upToDate" && (
|
{state.status === "upToDate" && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue