From f29a019e02cf785b295a61da7ca0029c3e7907ed Mon Sep 17 00:00:00 2001 From: Bekiboo Date: Fri, 25 Sep 2026 13:48:20 +0300 Subject: [PATCH 1/2] fix(eid-wallet): show the role once, and a real message for an unknown eName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The details sheet printed the role as a badge and again as each entry's heading. The entry now repeats it only when it differs between entries or when it prefixes an awaiting note. resolveVaultUri threw "Registry could not resolve @…: 404", which scanLogic showed verbatim: internal wording, an HTTP status, and untranslated. It now throws an ENAME_NOT_FOUND code, callers word it from the message catalog, and the status stays in console.error. --- docs/static/translations.json | 6 +++ infrastructure/eid-wallet/messages/en.json | 2 + infrastructure/eid-wallet/messages/ru.json | 2 + infrastructure/eid-wallet/messages/uk.json | 2 + .../src/lib/utils/socialBinding.spec.ts | 38 ++++++++++++++++ .../eid-wallet/src/lib/utils/socialBinding.ts | 23 +++++++--- .../SocialBindingDetailsSheet.svelte | 45 +++++++++++-------- .../src/routes/(app)/scan-qr/scanLogic.ts | 13 +++++- 8 files changed, 107 insertions(+), 24 deletions(-) diff --git a/docs/static/translations.json b/docs/static/translations.json index bce36cfcc..8772ecc5a 100644 --- a/docs/static/translations.json +++ b/docs/static/translations.json @@ -449,6 +449,7 @@ "scan_error_reveal_failed": "Failed to reveal vote", "scan_error_sign_failed": "Failed to sign. Please try again.", "scan_error_social_create": "Failed to create social binding.", + "scan_error_social_ename_not_found": "This eID could not be found. Please check the QR code and try again.", "scan_error_social_no_vault": "No active vault", "scan_error_social_parse": "Failed to process social binding request.", "scan_error_social_self": "You cannot create a social binding with yourself.", @@ -526,6 +527,7 @@ "social_drawer_success_title": "Binding Complete!", "social_drawer_they_said": "They said", "social_drawer_your_contact": "Your contact", + "social_ename_not_found": "This eID could not be found.", "social_role_received": "Received", "social_role_sent": "Sent", "social_role_sent_received": "Sent & Received", @@ -989,6 +991,7 @@ "scan_error_reveal_failed": "Не удалось раскрыть голос", "scan_error_sign_failed": "Не удалось подписать. Попробуйте ещё раз.", "scan_error_social_create": "Не удалось создать социальную связь.", + "scan_error_social_ename_not_found": "Этот eID не найден. Проверьте QR-код и попробуйте снова.", "scan_error_social_no_vault": "Активное хранилище отсутствует", "scan_error_social_parse": "Не удалось обработать запрос на социальную связь.", "scan_error_social_self": "Нельзя создать социальную связь с самим собой.", @@ -1066,6 +1069,7 @@ "social_drawer_success_title": "Связь установлена!", "social_drawer_they_said": "Комментарий", "social_drawer_your_contact": "Ваш контакт", + "social_ename_not_found": "Этот eID не найден.", "social_role_received": "Получено", "social_role_sent": "Отправлено", "social_role_sent_received": "Отправлено и получено", @@ -1529,6 +1533,7 @@ "scan_error_reveal_failed": "Не вдалося розкрити голос", "scan_error_sign_failed": "Не вдалося підписати. Спробуйте ще раз.", "scan_error_social_create": "Не вдалося створити соціальний зв’язок.", + "scan_error_social_ename_not_found": "Цей eID не знайдено. Перевірте QR-код і спробуйте ще раз.", "scan_error_social_no_vault": "Активного сховища немає", "scan_error_social_parse": "Не вдалося обробити запит на соціальний зв’язок.", "scan_error_social_self": "Не можна створити соціальний зв’язок із самим собою.", @@ -1606,6 +1611,7 @@ "social_drawer_success_title": "Зв’язок встановлено!", "social_drawer_they_said": "Коментар", "social_drawer_your_contact": "Ваш контакт", + "social_ename_not_found": "Цей eID не знайдено.", "social_role_received": "Отримано", "social_role_sent": "Надіслано", "social_role_sent_received": "Надіслано та отримано", diff --git a/infrastructure/eid-wallet/messages/en.json b/infrastructure/eid-wallet/messages/en.json index 02041cec7..9f22f245e 100644 --- a/infrastructure/eid-wallet/messages/en.json +++ b/infrastructure/eid-wallet/messages/en.json @@ -568,6 +568,7 @@ "scan_error_reveal_failed": "Failed to reveal vote", "scan_error_sign_failed": "Failed to sign. Please try again.", "scan_error_social_create": "Failed to create social binding.", + "scan_error_social_ename_not_found": "This eID could not be found. Please check the QR code and try again.", "scan_error_social_no_vault": "No active vault", "scan_error_social_parse": "Failed to process social binding request.", "scan_error_social_self": "You cannot create a social binding with yourself.", @@ -676,6 +677,7 @@ "social_drawer_success_title": "Binding Complete!", "social_drawer_they_said": "They said", "social_drawer_your_contact": "Your contact", + "social_ename_not_found": "This eID could not be found.", "social_role_received": "Received", "social_role_sent": "Sent", "social_role_sent_received": "Sent & Received", diff --git a/infrastructure/eid-wallet/messages/ru.json b/infrastructure/eid-wallet/messages/ru.json index 8f37761cf..d911fea60 100644 --- a/infrastructure/eid-wallet/messages/ru.json +++ b/infrastructure/eid-wallet/messages/ru.json @@ -584,6 +584,7 @@ "scan_error_reveal_failed": "Не удалось раскрыть голос", "scan_error_sign_failed": "Не удалось подписать. Попробуйте ещё раз.", "scan_error_social_create": "Не удалось создать социальную связь.", + "scan_error_social_ename_not_found": "Этот eID не найден. Проверьте QR-код и попробуйте снова.", "scan_error_social_no_vault": "Активное хранилище отсутствует", "scan_error_social_parse": "Не удалось обработать запрос на социальную связь.", "scan_error_social_self": "Нельзя создать социальную связь с самим собой.", @@ -696,6 +697,7 @@ "social_drawer_success_title": "Связь установлена!", "social_drawer_they_said": "Комментарий", "social_drawer_your_contact": "Ваш контакт", + "social_ename_not_found": "Этот eID не найден.", "social_role_received": "Получено", "social_role_sent": "Отправлено", "social_role_sent_received": "Отправлено и получено", diff --git a/infrastructure/eid-wallet/messages/uk.json b/infrastructure/eid-wallet/messages/uk.json index 75bde60e0..1d3839580 100644 --- a/infrastructure/eid-wallet/messages/uk.json +++ b/infrastructure/eid-wallet/messages/uk.json @@ -584,6 +584,7 @@ "scan_error_reveal_failed": "Не вдалося розкрити голос", "scan_error_sign_failed": "Не вдалося підписати. Спробуйте ще раз.", "scan_error_social_create": "Не вдалося створити соціальний зв’язок.", + "scan_error_social_ename_not_found": "Цей eID не знайдено. Перевірте QR-код і спробуйте ще раз.", "scan_error_social_no_vault": "Активного сховища немає", "scan_error_social_parse": "Не вдалося обробити запит на соціальний зв’язок.", "scan_error_social_self": "Не можна створити соціальний зв’язок із самим собою.", @@ -696,6 +697,7 @@ "social_drawer_success_title": "Зв’язок встановлено!", "social_drawer_they_said": "Коментар", "social_drawer_your_contact": "Ваш контакт", + "social_ename_not_found": "Цей eID не знайдено.", "social_role_received": "Отримано", "social_role_sent": "Надіслано", "social_role_sent_received": "Надіслано та отримано", diff --git a/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts b/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts index e0827905a..9de23409b 100644 --- a/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts +++ b/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts @@ -7,12 +7,14 @@ vi.mock("$env/static/public", () => ({ import { CANCEL_NOT_PENDING, + ENAME_NOT_FOUND, acceptSocialBinding, cancelSentSocialBinding, declineSocialBinding, fetchReconciledSocialBindings, fetchSocialBindings, fetchUnsignedSocialDocs, + resolveVaultUri, } from "./socialBinding"; const ME = "@me"; @@ -436,3 +438,39 @@ describe("fetchSocialBindings", () => { expect(summary.parsed.data.relation_description).toBe("hi"); }); }); + +describe("resolveVaultUri", () => { + beforeEach(() => { + vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.mocked(console.error).mockRestore(); + }); + + // The throw reaches a user-facing error box, so it carries a code the + // caller can translate and nothing the registry said. + it("reports an unknown eName as a code, not as a status line", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ ok: false, status: 404 }) as Response), + ); + + await expect(resolveVaultUri("@nobody")).rejects.toThrow( + ENAME_NOT_FOUND, + ); + }); + + it("reports a registry answer without a URI the same way", async () => { + vi.stubGlobal( + "fetch", + vi.fn( + async () => ({ ok: true, json: async () => ({}) }) as Response, + ), + ); + + await expect(resolveVaultUri("@nobody")).rejects.toThrow( + ENAME_NOT_FOUND, + ); + }); +}); diff --git a/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts b/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts index bc5481428..332814b48 100644 --- a/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts +++ b/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts @@ -30,6 +30,13 @@ export interface BindingDocEdge { // Registry resolution // --------------------------------------------------------------------------- +/** + * The registry has no eVault for this eName, as a code rather than a sentence: + * this module has no i18n, so the caller renders the wording. The status and + * the eName stay in the console. + */ +export const ENAME_NOT_FOUND = "social-binding/ename-not-found"; + /** * Resolve an eName to its eVault GraphQL endpoint via the registry. */ @@ -40,13 +47,19 @@ export async function resolveVaultUri(ename: string): Promise { PUBLIC_REGISTRY_URL, ); const res = await fetch(url.toString(), { method: "GET" }); - if (!res.ok) - throw new Error( - `Registry could not resolve ${normalized}: ${res.status}`, + if (!res.ok) { + console.error( + `[socialBinding] registry could not resolve ${normalized}: ${res.status}`, ); + throw new Error(ENAME_NOT_FOUND); + } const json = await res.json(); - if (!json?.uri) - throw new Error(`Registry returned no URI for ${normalized}`); + if (!json?.uri) { + console.error( + `[socialBinding] registry returned no URI for ${normalized}`, + ); + throw new Error(ENAME_NOT_FOUND); + } const base = json.uri as string; return base.endsWith("/graphql") ? base diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/SocialBindingDetailsSheet.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/SocialBindingDetailsSheet.svelte index 43eebf5fe..fea5563a7 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/components/SocialBindingDetailsSheet.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/SocialBindingDetailsSheet.svelte @@ -5,6 +5,7 @@ import { getLocale } from "$lib/paraglide/runtime"; import { BottomSheet, ButtonAction } from "$lib/ui"; import { CANCEL_NOT_PENDING, + ENAME_NOT_FOUND, type SocialBindingSummary, acceptSocialBinding, cancelSentSocialBinding, @@ -96,12 +97,13 @@ async function callerContext(): Promise { /** * socialBinding.ts carries no i18n so it stays testable without the app's module - * aliases; its one user-facing refusal arrives as a code and is worded here. + * aliases; its user-facing refusals arrive as codes and are worded here. */ function messageFor(err: Error): string { - return err.message === CANCEL_NOT_PENDING - ? m.social_cancel_not_pending() - : err.message; + if (err.message === CANCEL_NOT_PENDING) + return m.social_cancel_not_pending(); + if (err.message === ENAME_NOT_FOUND) return m.social_ename_not_found(); + return err.message; } async function runAction( @@ -186,24 +188,31 @@ function cancel(binding: SocialBindingSummary) { binding.role === "received" && !binding.mutuallySigned} {@const awaitingThem = binding.role === "sent" && !binding.mutuallySigned} + + {@const showRole = + contact.role === "both" || !binding.mutuallySigned}
-

- {binding.role === "sent" - ? m.social_role_sent() - : m.social_role_received()} - {#if needsMyConfirmation} - {m.social_details_awaiting_you_suffix()} - {:else if awaitingThem} - {m.social_details_awaiting_suffix()} - {/if} -

+ {#if showRole} +

+ {binding.role === "sent" + ? m.social_role_sent() + : m.social_role_received()} + {#if needsMyConfirmation} + {m.social_details_awaiting_you_suffix()} + {:else if awaitingThem} + {m.social_details_awaiting_suffix()} + {/if} +

+ {/if} {#if binding.relationDescription}

Date: Fri, 25 Sep 2026 16:02:14 +0300 Subject: [PATCH 2/2] fix(eid-wallet): only report a registry 404 as an unknown eName resolveVaultUri mapped every failed registry response to ENAME_NOT_FOUND, so a registry outage (500, or a proxy's 502/503) told the user the eID did not exist and to check a QR code that was fine. Any status other than 404 now throws REGISTRY_UNAVAILABLE, which callers word with the existing generic messages. --- .../eid-wallet/src/lib/utils/socialBinding.spec.ts | 12 ++++++++++++ .../eid-wallet/src/lib/utils/socialBinding.ts | 7 ++++++- .../components/SocialBindingDetailsSheet.svelte | 3 +++ .../src/routes/(app)/scan-qr/scanLogic.ts | 13 ++++++++----- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts b/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts index 9de23409b..b9ee1ccca 100644 --- a/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts +++ b/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts @@ -8,6 +8,7 @@ vi.mock("$env/static/public", () => ({ import { CANCEL_NOT_PENDING, ENAME_NOT_FOUND, + REGISTRY_UNAVAILABLE, acceptSocialBinding, cancelSentSocialBinding, declineSocialBinding, @@ -461,6 +462,17 @@ describe("resolveVaultUri", () => { ); }); + it("does not report a registry outage as an unknown eName", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => ({ ok: false, status: 503 }) as Response), + ); + + await expect(resolveVaultUri("@alice")).rejects.toMatchObject({ + message: REGISTRY_UNAVAILABLE, + }); + }); + it("reports a registry answer without a URI the same way", async () => { vi.stubGlobal( "fetch", diff --git a/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts b/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts index 332814b48..66fec6f9d 100644 --- a/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts +++ b/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts @@ -37,6 +37,9 @@ export interface BindingDocEdge { */ export const ENAME_NOT_FOUND = "social-binding/ename-not-found"; +/** The registry failed for another reason (5xx, a proxy error): no verdict on the eName. */ +export const REGISTRY_UNAVAILABLE = "social-binding/registry-unavailable"; + /** * Resolve an eName to its eVault GraphQL endpoint via the registry. */ @@ -51,7 +54,9 @@ export async function resolveVaultUri(ename: string): Promise { console.error( `[socialBinding] registry could not resolve ${normalized}: ${res.status}`, ); - throw new Error(ENAME_NOT_FOUND); + throw new Error( + res.status === 404 ? ENAME_NOT_FOUND : REGISTRY_UNAVAILABLE, + ); } const json = await res.json(); if (!json?.uri) { diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/SocialBindingDetailsSheet.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/SocialBindingDetailsSheet.svelte index fea5563a7..74954574c 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/components/SocialBindingDetailsSheet.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/SocialBindingDetailsSheet.svelte @@ -6,6 +6,7 @@ import { BottomSheet, ButtonAction } from "$lib/ui"; import { CANCEL_NOT_PENDING, ENAME_NOT_FOUND, + REGISTRY_UNAVAILABLE, type SocialBindingSummary, acceptSocialBinding, cancelSentSocialBinding, @@ -103,6 +104,8 @@ function messageFor(err: Error): string { if (err.message === CANCEL_NOT_PENDING) return m.social_cancel_not_pending(); if (err.message === ENAME_NOT_FOUND) return m.social_ename_not_found(); + if (err.message === REGISTRY_UNAVAILABLE) + return m.social_drawer_error_generic(); return err.message; } diff --git a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts index 25f523dae..81918b506 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts +++ b/infrastructure/eid-wallet/src/routes/(app)/scan-qr/scanLogic.ts @@ -19,6 +19,7 @@ import { authenticate } from "wallet-sdk"; import type { GlobalState } from "$lib/global"; import { ENAME_NOT_FOUND, + REGISTRY_UNAVAILABLE, createOwnSocialBindingMirror, createSocialConnectionDoc, fetchNameFromVault, @@ -708,13 +709,15 @@ export function createScanLogic({ } /** - * socialBinding.ts carries no i18n; its user-facing refusal arrives as a - * code and is worded here. + * socialBinding.ts carries no i18n; its user-facing refusals arrive as + * codes and are worded here. */ function socialBindingMessageFor(err: Error): string { - return err.message === ENAME_NOT_FOUND - ? m.scan_error_social_ename_not_found() - : err.message; + if (err.message === ENAME_NOT_FOUND) + return m.scan_error_social_ename_not_found(); + if (err.message === REGISTRY_UNAVAILABLE) + return m.scan_error_social_create(); + return err.message; } async function handleSocialBinding() {