diff --git a/rfirma-app/po/ca.po b/rfirma-app/po/ca.po
index 00223e58..b51258b4 100644
--- a/rfirma-app/po/ca.po
+++ b/rfirma-app/po/ca.po
@@ -1052,6 +1052,9 @@ msgstr "Fitxer instal·lat"
msgid "status.storeBrands.windows"
msgstr "Magatzem de Windows"
+msgid "status.storeBrands.macos"
+msgstr "Magatzem de macOS"
+
msgid "status.notices.restartFirefox"
msgstr "Reinicia el Firefox perquè tingui efecte."
diff --git a/rfirma-app/po/en.po b/rfirma-app/po/en.po
index 28a455aa..b045a3f8 100644
--- a/rfirma-app/po/en.po
+++ b/rfirma-app/po/en.po
@@ -1052,6 +1052,9 @@ msgstr "Installed file"
msgid "status.storeBrands.windows"
msgstr "Windows store"
+msgid "status.storeBrands.macos"
+msgstr "macOS store"
+
msgid "status.notices.restartFirefox"
msgstr "Restart Firefox for this to take effect."
diff --git a/rfirma-app/po/es.po b/rfirma-app/po/es.po
index ebf068e7..887a98ea 100644
--- a/rfirma-app/po/es.po
+++ b/rfirma-app/po/es.po
@@ -1052,6 +1052,9 @@ msgstr "Fichero instalado"
msgid "status.storeBrands.windows"
msgstr "Almacén de Windows"
+msgid "status.storeBrands.macos"
+msgstr "Almacén de macOS"
+
msgid "status.notices.restartFirefox"
msgstr "Reinicia Firefox para que surta efecto."
diff --git a/rfirma-app/po/eu.po b/rfirma-app/po/eu.po
index 87e6be35..00a4e188 100644
--- a/rfirma-app/po/eu.po
+++ b/rfirma-app/po/eu.po
@@ -1053,6 +1053,9 @@ msgstr "Instalatutako fitxategia"
msgid "status.storeBrands.windows"
msgstr "Windows biltegia"
+msgid "status.storeBrands.macos"
+msgstr "macOS biltegia"
+
msgid "status.notices.restartFirefox"
msgstr "Berrabiarazi Firefox eragina izan dezan."
diff --git a/rfirma-app/po/gl.po b/rfirma-app/po/gl.po
index 9faa93a9..a080228c 100644
--- a/rfirma-app/po/gl.po
+++ b/rfirma-app/po/gl.po
@@ -1050,6 +1050,9 @@ msgstr "Ficheiro instalado"
msgid "status.storeBrands.windows"
msgstr "Almacén de Windows"
+msgid "status.storeBrands.macos"
+msgstr "Almacén de macOS"
+
msgid "status.notices.restartFirefox"
msgstr "Reinicia Firefox para que xurda efecto."
diff --git a/rfirma-app/po/messages.pot b/rfirma-app/po/messages.pot
index a2eae7ab..934330c3 100644
--- a/rfirma-app/po/messages.pot
+++ b/rfirma-app/po/messages.pot
@@ -1037,6 +1037,9 @@ msgstr ""
msgid "status.storeBrands.windows"
msgstr ""
+msgid "status.storeBrands.macos"
+msgstr ""
+
msgid "status.notices.restartFirefox"
msgstr ""
diff --git a/rfirma-app/src/setup/SetupWizard.presentation.test.tsx b/rfirma-app/src/setup/SetupWizard.presentation.test.tsx
index cf51d47f..33f328e6 100644
--- a/rfirma-app/src/setup/SetupWizard.presentation.test.tsx
+++ b/rfirma-app/src/setup/SetupWizard.presentation.test.tsx
@@ -1,5 +1,6 @@
import { composeStories } from "@storybook/react-vite";
import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { renderWithCatalog } from "../testing/render";
import * as stories from "./SetupWizard.stories";
@@ -13,6 +14,7 @@ const {
CertificateDoneHandlerPending,
WithoutAutoFirma,
ProtectionOff,
+ CertificateFailedOnMacos,
} = composeStories(stories);
const stepButtons = ["Instalar", "Usar rFirma", "Ahora no"];
@@ -99,3 +101,13 @@ describe("the protection against accidental signing", () => {
expect(await screen.findByRole("switch")).not.toBeChecked();
});
});
+
+describe("the certificate step on macOS", () => {
+ it("names the macOS store among the places where the install failed", async () => {
+ renderWithCatalog();
+
+ await userEvent.click(await screen.findByRole("button", { name: "Instalar" }));
+
+ expect(await screen.findByText("Almacén de macOS")).toBeInTheDocument();
+ });
+});
diff --git a/rfirma-app/src/setup/SetupWizard.stories.tsx b/rfirma-app/src/setup/SetupWizard.stories.tsx
index 69b0a3f4..c490dba3 100644
--- a/rfirma-app/src/setup/SetupWizard.stories.tsx
+++ b/rfirma-app/src/setup/SetupWizard.stories.tsx
@@ -1,13 +1,14 @@
//! Las historias del asistente del primer arranque: la bienvenida y la configuración con cada estado de sus dos pasos.
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { fn } from "storybook/test";
+import { expect, fn, userEvent, within } from "storybook/test";
import { inMemoryPreferences } from "../preferences/preferences";
import { defaults } from "../preferences/testing/fixtures";
import { memoryStatus } from "../status/status";
import { SetupWizard } from "./SetupWizard";
import {
aVersionRow,
+ certificateFailedOnMacos,
certificateInstalled,
certificateNotInstalled,
handlerNotOurs,
@@ -85,6 +86,22 @@ export const WithoutAutoFirma: Story = {
},
};
+export const CertificateFailedOnMacos: Story = {
+ name: "2 · Configuración, certificado sin instalar en macOS",
+ args: {
+ initialStep: 2,
+ statusPort: {
+ ...memoryStatus([aVersionRow, certificateNotInstalled, handlerNotOurs]),
+ installLocalCaCertificate: async () => certificateFailedOnMacos,
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await userEvent.click(await canvas.findByRole("button", { name: "Instalar" }));
+ await expect(await canvas.findByText("Almacén de macOS")).toBeVisible();
+ },
+};
+
export const ProtectionOff: Story = {
name: "2 · Configuración, sin la espera de la sede",
args: {
diff --git a/rfirma-app/src/setup/testing/fixtures.ts b/rfirma-app/src/setup/testing/fixtures.ts
index 568c30f5..a24c2848 100644
--- a/rfirma-app/src/setup/testing/fixtures.ts
+++ b/rfirma-app/src/setup/testing/fixtures.ts
@@ -28,6 +28,12 @@ export const certificateInstalled: SignalRow = {
action: null,
};
+export const certificateFailedOnMacos: SignalRow = {
+ ...certificateNotInstalled,
+ verdict: "incorrect",
+ detail: { kind: "trust", stores: [{ brand: "macos", trusted: false }] },
+};
+
export const handlerNotOurs: SignalRow = {
signal: "siteSignature",
value: "AutoFirma",
diff --git a/rfirma-app/src/signing/CertificateCard.tsx b/rfirma-app/src/signing/CertificateCard.tsx
index 4b201392..59c2d359 100644
--- a/rfirma-app/src/signing/CertificateCard.tsx
+++ b/rfirma-app/src/signing/CertificateCard.tsx
@@ -67,6 +67,8 @@ export function storeLabel(store: Store, t: TFunction): string {
return t("panel.certificate.stores.installed");
case "windows":
return t("status.storeBrands.windows");
+ case "macos":
+ return t("status.storeBrands.macos");
}
}
diff --git a/rfirma-app/src/signing/CertificateSelect.stories.tsx b/rfirma-app/src/signing/CertificateSelect.stories.tsx
index 9dfc2a79..86791228 100644
--- a/rfirma-app/src/signing/CertificateSelect.stories.tsx
+++ b/rfirma-app/src/signing/CertificateSelect.stories.tsx
@@ -5,6 +5,7 @@ import { fn } from "storybook/test";
import { CertificateSelect } from "./CertificateSelect";
import {
ENTITY_CERTIFICATE,
+ MACOS_CERTIFICATE,
PERSONAL_CERTIFICATE,
STORY_CERTIFICATES,
} from "./testing/storyReports";
@@ -53,4 +54,12 @@ export const Searching: Story = { args: { certificates: [], chosen: null, search
export const Disabled: Story = { args: { disabled: true } };
+export const OpenWithMacosStore: Story = {
+ args: {
+ certificates: [MACOS_CERTIFICATE, PERSONAL_CERTIFICATE],
+ chosen: MACOS_CERTIFICATE,
+ defaultOpen: true,
+ },
+};
+
export const Open: Story = { args: { defaultOpen: true } };
diff --git a/rfirma-app/src/signing/CertificateSelect.test.tsx b/rfirma-app/src/signing/CertificateSelect.test.tsx
index eaa55da9..2478cbc4 100644
--- a/rfirma-app/src/signing/CertificateSelect.test.tsx
+++ b/rfirma-app/src/signing/CertificateSelect.test.tsx
@@ -247,6 +247,16 @@ describe("CertificateSelect", () => {
expect(within(row(0)).getByText("Tarjeta")).toBeVisible();
});
+ it("names the macOS personal store as the macOS store", async () => {
+ renderSelect({
+ certificates: [aCertificate({ id: "macos", stores: ["macos"] })],
+ });
+
+ await userEvent.click(box());
+
+ expect(within(row(0)).getByText("Almacén de macOS")).toBeVisible();
+ });
+
it("tags a DNIe certificate as «DNIe» and a certificate on any other card as «Tarjeta»", async () => {
renderSelect({
certificates: [
diff --git a/rfirma-app/src/signing/certificate.ts b/rfirma-app/src/signing/certificate.ts
index 1ad27b8c..af911cd4 100644
--- a/rfirma-app/src/signing/certificate.ts
+++ b/rfirma-app/src/signing/certificate.ts
@@ -55,6 +55,7 @@ type CertificateStoreClass =
| "nssdb"
| "installed"
| "windows"
+ | "macos"
| BrowserStoreMark;
/** Un certificado elegible, con lo justo para pintarlo y para firmar con él. */
diff --git a/rfirma-app/src/signing/testing/storyReports.ts b/rfirma-app/src/signing/testing/storyReports.ts
index 99fbdb33..6afc7236 100644
--- a/rfirma-app/src/signing/testing/storyReports.ts
+++ b/rfirma-app/src/signing/testing/storyReports.ts
@@ -181,6 +181,15 @@ const NOT_YET_VALID_CERTIFICATE = aCertificate({
status: { kind: "notYetValid", notBefore: IN_2099 },
});
+export const MACOS_CERTIFICATE = aCertificate({
+ id: "macos",
+ holderName: "BABBAGE CHARLES",
+ givenName: "Charles",
+ surname: "Babbage",
+ idNumber: "IDCES-00000002W",
+ stores: ["macos"],
+});
+
export const STORY_CERTIFICATES: readonly Certificate[] = [
PERSONAL_CERTIFICATE,
ENTITY_CERTIFICATE,
diff --git a/rfirma-app/src/status/StatusView.presentation.test.tsx b/rfirma-app/src/status/StatusView.presentation.test.tsx
index 2c0ddd3e..9d6b8a6d 100644
--- a/rfirma-app/src/status/StatusView.presentation.test.tsx
+++ b/rfirma-app/src/status/StatusView.presentation.test.tsx
@@ -118,6 +118,11 @@ const table: Record = {
buttons: [],
absent: ["No aplica"],
},
+ UserCertificatesOnMacos: {
+ texts: ["Almacén de macOS", "Tarjeta"],
+ buttons: ["Ver dónde"],
+ absent: ["Almacén de Windows"],
+ },
UserCertificatesDetailExpanded: {
texts: [
"Almacén de Windows",
diff --git a/rfirma-app/src/status/StatusView.stories.tsx b/rfirma-app/src/status/StatusView.stories.tsx
index 5a2d7d5d..a764bdda 100644
--- a/rfirma-app/src/status/StatusView.stories.tsx
+++ b/rfirma-app/src/status/StatusView.stories.tsx
@@ -21,6 +21,7 @@ import {
sitesUnavailableOutsideFlatpak,
sitesWithTwoCandidates,
someUserCertificates,
+ someUserCertificatesOnMacos,
versionChecking,
versionOutdated,
versionUpToDate,
@@ -122,6 +123,10 @@ export const CardReaderMissing = panel([cardReaderMissing]);
export const CardReaderNotSupported = panel([cardReaderNotSupported]);
+export const UserCertificatesOnMacos = panel([someUserCertificatesOnMacos], {
+ initiallyExpanded: ["userCertificates"],
+});
+
export const UserCertificatesDetailExpanded = panel([someUserCertificates], {
initiallyExpanded: ["userCertificates"],
});
diff --git a/rfirma-app/src/status/status.ts b/rfirma-app/src/status/status.ts
index 703770a9..bcc5aa34 100644
--- a/rfirma-app/src/status/status.ts
+++ b/rfirma-app/src/status/status.ts
@@ -21,7 +21,8 @@ export type StoreBrand =
| "nssdb"
| "card"
| "installed"
- | "windows";
+ | "windows"
+ | "macos";
export interface StoreDetail {
brand: StoreBrand;
@@ -44,6 +45,8 @@ export function storeBrandLabel(t: TFunction, brand: StoreBrand): string {
return t("status.storeBrands.installed");
case "windows":
return t("status.storeBrands.windows");
+ case "macos":
+ return t("status.storeBrands.macos");
}
}
diff --git a/rfirma-app/src/status/testing/fixtures.ts b/rfirma-app/src/status/testing/fixtures.ts
index 917c7c4a..46e1e0f3 100644
--- a/rfirma-app/src/status/testing/fixtures.ts
+++ b/rfirma-app/src/status/testing/fixtures.ts
@@ -148,6 +148,17 @@ export const someUserCertificates: SignalRow = {
},
};
+export const someUserCertificatesOnMacos: SignalRow = {
+ ...someUserCertificates,
+ detail: {
+ kind: "certificates",
+ stores: [
+ { brand: "macos", certificates: 2 },
+ { brand: "card", certificates: 1 },
+ ],
+ },
+};
+
export const cardReaderDetected = cardReaderRow({ kind: "noCard" });
export const cardReaderMissing = cardReaderRow({ kind: "noReader" });