From 6d67ab8935671edaadc9916b747001adc2aaaa21 Mon Sep 17 00:00:00 2001
From: le king fu
Date: Thu, 9 Apr 2026 08:58:51 -0400
Subject: [PATCH] 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.
---
CHANGELOG.fr.md | 3 +++
CHANGELOG.md | 3 +++
src/components/shared/ErrorPage.tsx | 15 ++++++++++++++-
src/hooks/useUpdater.ts | 15 +++++++++++++++
src/i18n/locales/en.json | 4 +++-
src/i18n/locales/fr.json | 4 +++-
src/pages/SettingsPage.tsx | 8 ++++++++
7 files changed, 49 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md
index e6b4701..0563e56 100644
--- a/CHANGELOG.fr.md
+++ b/CHANGELOG.fr.md
@@ -5,6 +5,9 @@
### 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)
+### 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
### Modifié
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 391185b..088a5c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@
### 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)
+### 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
### Changed
diff --git a/src/components/shared/ErrorPage.tsx b/src/components/shared/ErrorPage.tsx
index 630c95b..0cfbca8 100644
--- a/src/components/shared/ErrorPage.tsx
+++ b/src/components/shared/ErrorPage.tsx
@@ -2,6 +2,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { AlertTriangle, ChevronDown, ChevronUp, RefreshCw, Download, Mail, Bug } from "lucide-react";
import { check } from "@tauri-apps/plugin-updater";
+import { invoke } from "@tauri-apps/api/core";
interface ErrorPageProps {
error?: string;
@@ -10,7 +11,7 @@ interface ErrorPageProps {
export default function ErrorPage({ error }: ErrorPageProps) {
const { t } = useTranslation();
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(null);
const [updateError, setUpdateError] = useState(null);
@@ -18,6 +19,13 @@ export default function ErrorPage({ error }: ErrorPageProps) {
setUpdateStatus("checking");
setUpdateError(null);
try {
+ const allowed = await invoke("check_entitlement", {
+ feature: "auto-update",
+ });
+ if (!allowed) {
+ setUpdateStatus("notEntitled");
+ return;
+ }
const update = await check();
if (update) {
setUpdateStatus("available");
@@ -89,6 +97,11 @@ export default function ErrorPage({ error }: ErrorPageProps) {
{t("error.upToDate")}
)}
+ {updateStatus === "notEntitled" && (
+
+ {t("error.updateNotEntitled")}
+
+ )}
{updateStatus === "error" && updateError && (
{updateError}
)}
diff --git a/src/hooks/useUpdater.ts b/src/hooks/useUpdater.ts
index b694786..9bf9f34 100644
--- a/src/hooks/useUpdater.ts
+++ b/src/hooks/useUpdater.ts
@@ -1,6 +1,7 @@
import { useReducer, useCallback, useRef } from "react";
import { check, type Update } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
+import { invoke } from "@tauri-apps/api/core";
type UpdateStatus =
| "idle"
@@ -10,6 +11,7 @@ type UpdateStatus =
| "downloading"
| "readyToInstall"
| "installing"
+ | "notEntitled"
| "error";
interface UpdaterState {
@@ -29,6 +31,7 @@ type UpdaterAction =
| { type: "DOWNLOAD_PROGRESS"; downloaded: number; contentLength: number | null }
| { type: "READY_TO_INSTALL" }
| { type: "INSTALLING" }
+ | { type: "NOT_ENTITLED" }
| { type: "ERROR"; error: string };
const initialState: UpdaterState = {
@@ -56,6 +59,8 @@ function reducer(state: UpdaterState, action: UpdaterAction): UpdaterState {
return { ...state, status: "readyToInstall", error: null };
case "INSTALLING":
return { ...state, status: "installing", error: null };
+ case "NOT_ENTITLED":
+ return { ...state, status: "notEntitled", error: null };
case "ERROR":
return { ...state, status: "error", error: action.error };
}
@@ -68,6 +73,16 @@ export function useUpdater() {
const checkForUpdate = useCallback(async () => {
dispatch({ type: "CHECK_START" });
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("check_entitlement", {
+ feature: "auto-update",
+ });
+ if (!allowed) {
+ dispatch({ type: "NOT_ENTITLED" });
+ return;
+ }
const update = await check();
if (update) {
updateRef.current = update;
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index e7b4011..f078f6b 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -436,7 +436,8 @@
"installing": "Installing...",
"error": "Update failed",
"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": {
"title": "Data Management",
@@ -827,6 +828,7 @@
"checkUpdate": "Check for updates",
"updateAvailable": "Update available: v{{version}}",
"upToDate": "The application is up to date",
+ "updateNotEntitled": "Automatic updates are available with the Base edition.",
"contactUs": "Contact us",
"contactEmail": "Send an email to",
"reportIssue": "Report an issue"
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index af3cba4..51a9b40 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -436,7 +436,8 @@
"installing": "Installation en cours...",
"error": "Erreur lors de la mise à jour",
"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": {
"title": "Gestion des données",
@@ -827,6 +828,7 @@
"checkUpdate": "Vérifier les mises à jour",
"updateAvailable": "Mise à jour disponible : v{{version}}",
"upToDate": "L'application est à jour",
+ "updateNotEntitled": "Les mises à jour automatiques sont disponibles avec l'édition Base.",
"contactUs": "Nous contacter",
"contactEmail": "Envoyez un email à",
"reportIssue": "Signaler un problème"
diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx
index 6e56b16..b567155 100644
--- a/src/pages/SettingsPage.tsx
+++ b/src/pages/SettingsPage.tsx
@@ -155,6 +155,14 @@ export default function SettingsPage() {
)}
+ {/* not entitled (free edition) */}
+ {state.status === "notEntitled" && (
+
+
+
{t("settings.updates.notEntitled")}
+
+ )}
+
{/* up to date */}
{state.status === "upToDate" && (