diff --git a/src/components/shared/UpsellCtas.render.test.tsx b/src/components/shared/UpsellCtas.render.test.tsx
index a519790..ddf95d5 100644
--- a/src/components/shared/UpsellCtas.render.test.tsx
+++ b/src/components/shared/UpsellCtas.render.test.tsx
@@ -24,8 +24,8 @@ vi.mock("react-i18next", () => ({
}),
}));
-// The locked screen reads one thing from the license context: whether the stored key
-// was revoked (#365).
+// The locked screen and the Updates card read one thing from the license context:
+// whether the stored key was revoked (#365).
let licenseRevoked = false;
vi.mock("../../contexts/LicenseContext", () => ({
useLicenseContext: () => ({ revoked: licenseRevoked }),
@@ -161,5 +161,19 @@ describe("UpdateCard in the free edition", () => {
expect(html).toContain("upsell.ctaGet[tier=license.editions.base]");
expect(html).toContain("upsell.ctaHaveKey");
expect(html).toContain("settings.updates.downloadLatest");
+ expect(html).not.toContain("upsell.revoked");
+ });
+
+ it("names a revoked key between the message and the CTAs (#365)", () => {
+ licenseRevoked = true;
+ const html = renderToStaticMarkup(
+
+
+ ,
+ );
+ const line = html.indexOf("upsell.revoked");
+ expect(line).toBeGreaterThan(html.indexOf("settings.updates.notEntitled"));
+ expect(line).toBeLessThan(html.indexOf("upsell.ctaGet"));
+ expect(html.split("upsell.revoked")).toHaveLength(2);
});
});
diff --git a/src/contexts/LicenseContext.test.ts b/src/contexts/LicenseContext.test.ts
index f33e23c..011d28c 100644
--- a/src/contexts/LicenseContext.test.ts
+++ b/src/contexts/LicenseContext.test.ts
@@ -371,7 +371,7 @@ describe("performRevocationCheck", () => {
["revoked", "License check: the license server says the key was revoked"],
["unreachable", "License check: the license server could not be reached or answered with an error, nothing changed"],
["unknown", "License check: the license server gave no usable answer, nothing changed"],
- ["storage", "License check: the attempt could not be recorded, nothing changed"],
+ ["storage", "License check: the attempt or the server's answer could not be recorded, nothing changed"],
];
it("has a log message for every outcome, and no other", () => {
diff --git a/src/contexts/LicenseContext.tsx b/src/contexts/LicenseContext.tsx
index 2f2124c..d0bd264 100644
--- a/src/contexts/LicenseContext.tsx
+++ b/src/contexts/LicenseContext.tsx
@@ -179,7 +179,7 @@ export const REVOCATION_LOG: Record
= {
revoked: "License check: the license server says the key was revoked",
unreachable: "License check: the license server could not be reached or answered with an error, nothing changed",
unknown: "License check: the license server gave no usable answer, nothing changed",
- storage: "License check: the attempt could not be recorded, nothing changed",
+ storage: "License check: the attempt or the server's answer could not be recorded, nothing changed",
};
export interface RevocationCheckDeps {
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 27f422b..c568519 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -1126,8 +1126,8 @@
"Activating a machine binds it to your key on the license server; it only serves the market prices of the Balance (Premium), and your edition never depends on it. After a reinstall, a new computer, a different key or an expired activation, the card shows \"Not activated\": click \"Activate\"",
"Activation sends the license server your key, this computer's name and a derived identifier, specific to Simpl'Résultat, in place of the system's machine identifier",
"The machine list shows how many machines are activated out of the number your key allows; deactivate one to free a slot",
- "If the license server reports that your key was revoked, for instance after a refund, the app goes back to the Free edition and stays there from one session to the next: the License card and the locked screens say so. Your data is kept, and another valid key gives its edition back",
- "When a key is stored, the app asks the license server whether it was revoked: at startup, at most once every 24 hours, sending the product and the key, nothing else. Without a stored key no request is sent; if the server does not answer, nothing changes. Each check is written to the logs (Settings → System)"
+ "If the license server reports that your key was revoked, for instance after a refund, the app goes back to the Free edition and stays there from one session to the next: the License card, the locked screens and the Updates card say so. Your data is kept, and another valid key gives its edition back",
+ "When a key is stored, the app asks the license server whether it was revoked: at startup, at most once every 24 hours, sending the product and the key, nothing else. Without a stored key no request is sent; if the server does not answer, nothing changes. Each check is written to the logs (Settings → System). The check goes on after a revocation, as long as the key stays stored: a correction made by the license server is thus picked up at the next start. Only storing another key stops the previous one from being sent"
]
}
},
diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json
index 3035437..3fc77c4 100644
--- a/src/i18n/locales/fr.json
+++ b/src/i18n/locales/fr.json
@@ -1126,8 +1126,8 @@
"Activer une machine la lie à votre clé auprès du serveur de licences ; cela ne sert qu'aux cours du marché du Bilan (Premium), votre édition n'en dépend jamais. Après une réinstallation, un changement d'ordinateur, l'enregistrement d'une autre clé ou l'expiration de l'activation, la carte affiche « Non activée » : cliquez sur « Activer »",
"L'activation envoie au serveur de licences votre clé, le nom de cet ordinateur et un identifiant dérivé, propre à Simpl'Résultat, à la place de l'identifiant machine du système",
"La liste des machines indique combien sont activées sur le nombre permis par votre clé ; désactivez-en une pour libérer une place",
- "Si le serveur de licences signale que votre clé a été révoquée, par exemple après un remboursement, l'application repasse en édition Gratuite et le reste d'une session à l'autre : la carte Licence et les écrans verrouillés l'indiquent. Vos données sont conservées, et une autre clé valide redonne son édition",
- "Quand une clé est enregistrée, l'application vérifie auprès du serveur de licences qu'elle n'a pas été révoquée : au démarrage, au plus une fois par 24 heures, en envoyant le produit et la clé, rien d'autre. Sans clé enregistrée, aucune requête ne part ; si le serveur ne répond pas, rien ne change. Chaque vérification est inscrite dans les journaux (Paramètres → Système)"
+ "Si le serveur de licences signale que votre clé a été révoquée, par exemple après un remboursement, l'application repasse en édition Gratuite et le reste d'une session à l'autre : la carte Licence, les écrans verrouillés et la carte Mises à jour l'indiquent. Vos données sont conservées, et une autre clé valide redonne son édition",
+ "Quand une clé est enregistrée, l'application vérifie auprès du serveur de licences qu'elle n'a pas été révoquée : au démarrage, au plus une fois par 24 heures, en envoyant le produit et la clé, rien d'autre. Sans clé enregistrée, aucune requête ne part ; si le serveur ne répond pas, rien ne change. Chaque vérification est inscrite dans les journaux (Paramètres → Système). La vérification continue après une révocation, tant que la clé reste enregistrée : une correction faite par le serveur de licences est ainsi reprise au démarrage suivant. Seul l'enregistrement d'une autre clé arrête l'envoi de la précédente"
]
}
},
diff --git a/src/services/licenseService.ts b/src/services/licenseService.ts
index 70e1510..f340c6f 100644
--- a/src/services/licenseService.ts
+++ b/src/services/licenseService.ts
@@ -39,7 +39,8 @@ export async function checkEntitlement(feature: string): Promise {
/**
* How a revocation check ended (#365), as the Rust `RevocationOutcome` enum serializes.
- * `no_license`, `not_due` and `storage` mean nothing was sent.
+ * `no_license` and `not_due`: nothing was sent. `storage`: the attempt could not be
+ * written, so nothing was sent, or the server's verdict could not be, so nothing changed.
*/
export type RevocationOutcome =
| "no_license"