Merge pull request 'feat: gate auto-updates behind license entitlement (#48)' (#58) from issue-48-gate-auto-updates into issue-46-license-commands-entitlements
All checks were successful
PR Check / rust (push) Successful in 16m9s
PR Check / frontend (push) Successful in 2m11s
PR Check / rust (pull_request) Successful in 16m10s
PR Check / frontend (pull_request) Successful in 2m14s

This commit is contained in:
maximus 2026-04-10 13:55:39 +00:00
commit dd106a1df6
7 changed files with 49 additions and 3 deletions

View file

@ -6,6 +6,9 @@
- 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)
- Carte de licence dans les Paramètres : affiche l'édition actuelle (Gratuite/Base/Premium), accepte une clé de licence et redirige vers la page d'achat (#47) - Carte de licence dans les Paramètres : affiche l'édition actuelle (Gratuite/Base/Premium), accepte une clé de licence et redirige vers la page d'achat (#47)
### 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é

View file

@ -6,6 +6,9 @@
- 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)
- License card in Settings page: shows the current edition (Free/Base/Premium), accepts a license key, and links to the purchase page (#47) - License card in Settings page: shows the current edition (Free/Base/Premium), accepts a license key, and links to the purchase page (#47)
### 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

View file

@ -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>
)} )}

View file

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

View file

@ -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"

View file

@ -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"

View file

@ -159,6 +159,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">