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..b9ee1ccca 100644 --- a/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts +++ b/infrastructure/eid-wallet/src/lib/utils/socialBinding.spec.ts @@ -7,12 +7,15 @@ vi.mock("$env/static/public", () => ({ import { CANCEL_NOT_PENDING, + ENAME_NOT_FOUND, + REGISTRY_UNAVAILABLE, acceptSocialBinding, cancelSentSocialBinding, declineSocialBinding, fetchReconciledSocialBindings, fetchSocialBindings, fetchUnsignedSocialDocs, + resolveVaultUri, } from "./socialBinding"; const ME = "@me"; @@ -436,3 +439,50 @@ 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("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", + 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..66fec6f9d 100644 --- a/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts +++ b/infrastructure/eid-wallet/src/lib/utils/socialBinding.ts @@ -30,6 +30,16 @@ 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"; + +/** 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. */ @@ -40,13 +50,21 @@ export async function resolveVaultUri(ename: string): Promise { PUBLIC_REGISTRY_URL, ); const res = await fetch(url.toString(), { method: "GET" }); - if (!res.ok) + if (!res.ok) { + console.error( + `[socialBinding] registry could not resolve ${normalized}: ${res.status}`, + ); throw new Error( - `Registry could not resolve ${normalized}: ${res.status}`, + res.status === 404 ? ENAME_NOT_FOUND : REGISTRY_UNAVAILABLE, ); + } 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..74954574c 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,8 @@ import { getLocale } from "$lib/paraglide/runtime"; import { BottomSheet, ButtonAction } from "$lib/ui"; import { CANCEL_NOT_PENDING, + ENAME_NOT_FOUND, + REGISTRY_UNAVAILABLE, type SocialBindingSummary, acceptSocialBinding, cancelSentSocialBinding, @@ -96,12 +98,15 @@ 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(); + if (err.message === REGISTRY_UNAVAILABLE) + return m.social_drawer_error_generic(); + return err.message; } async function runAction( @@ -186,24 +191,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}