From 73ac0344765f5ca20cabda1bb23d586382da9d1f Mon Sep 17 00:00:00 2001 From: Bekiboo Date: Mon, 1 Jun 2026 15:36:01 +0300 Subject: [PATCH 1/7] Tighten home page top spacing --- infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte index dd1a777c1..22e2a3b75 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte @@ -715,7 +715,7 @@ async function refreshBindings(): Promise { {:else}
{#if pageReady}
Date: Mon, 1 Jun 2026 15:40:51 +0300 Subject: [PATCH 2/7] Add edit name button back --- .../src/routes/(app)/main/components/Greeting.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte index 1b98004f6..2fc05fad9 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte @@ -35,7 +35,7 @@ const { createBindingDocument today; there's no update mutation, and patching the underlying MetaEnvelope would invalidate the existing signature. Re-enable once that path lands. --> - + {/if}
From 0ac4b1eb6754b2c39ca84d55d7fdbcb794ccd21d Mon Sep 17 00:00:00 2001 From: Bekiboo Date: Tue, 2 Jun 2026 07:28:27 +0300 Subject: [PATCH 3/7] Use selfName for the displayed name --- .../src/routes/(app)/main/+page.svelte | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte index 22e2a3b75..af4afd3da 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte @@ -18,6 +18,7 @@ let cachedUserData: Record | undefined; let cachedEname: string | undefined; let cachedIsFake: boolean | undefined; let cachedLegalId: LegalIdDoc | null = null; +let cachedDisplayName: string | undefined; let cachedSocialBindingCount = 0; let cachedSocialBindingPreview: SocialBindingDisplay[] = []; let hasEverLoaded = false; @@ -88,6 +89,10 @@ let toastMessage = $state(""); // binding doc as the authoritative signal. let isFake = $state(cachedIsFake); let legalId = $state(cachedLegalId); +// Display name from the self-binding doc — the one the user typed at +// registration. Never gets overwritten by KYC the way userController.user.name +// does, so this stays as the user's chosen handle on the home greeting. +let displayName = $state(cachedDisplayName); let kycOpen = $state(false); let eVaultInfoOpen = $state(false); let bindingDocsInfoOpen = $state(false); @@ -152,12 +157,20 @@ async function loadBindingDocuments(): Promise { const edges: { node: { parsed: ParsedBindingDoc | null } }[] = json?.data?.bindingDocuments?.edges ?? []; - const idDoc = edges + const parsedDocs = edges .map((e) => e.node.parsed) - .find((p): p is ParsedBindingDoc => p?.type === "id_document"); + .filter((p): p is ParsedBindingDoc => p !== null); + const idDoc = parsedDocs.find((p) => p.type === "id_document"); legalId = idDoc ? toLegalIdDoc(idDoc) : null; cachedLegalId = legalId; + + const selfDoc = parsedDocs.find((p) => p.type === "self"); + const selfName = selfDoc?.data?.name; + if (typeof selfName === "string" && selfName.trim()) { + displayName = selfName.trim(); + cachedDisplayName = displayName; + } } catch (err) { console.warn("[main] Failed to load binding documents:", err); } @@ -725,7 +738,7 @@ async function refreshBindings(): Promise { > From d269cc1d77a75093f3c9162ba8d25db5fce894a3 Mon Sep 17 00:00:00 2001 From: Bekiboo Date: Tue, 2 Jun 2026 08:01:08 +0300 Subject: [PATCH 4/7] Change display name feature --- .../src/lib/utils/personalBinding.ts | 29 ++++ .../src/routes/(app)/main/+page.svelte | 147 ++++++++++++++++-- .../main/components/EditNameSheet.svelte | 101 ++++++++++++ .../(app)/main/components/Greeting.svelte | 5 - 4 files changed, 264 insertions(+), 18 deletions(-) create mode 100644 infrastructure/eid-wallet/src/routes/(app)/main/components/EditNameSheet.svelte diff --git a/infrastructure/eid-wallet/src/lib/utils/personalBinding.ts b/infrastructure/eid-wallet/src/lib/utils/personalBinding.ts index e5ec4f4df..548941920 100644 --- a/infrastructure/eid-wallet/src/lib/utils/personalBinding.ts +++ b/infrastructure/eid-wallet/src/lib/utils/personalBinding.ts @@ -176,6 +176,35 @@ export async function createSecurityQuestion( return unwrapCreate(result); } +/** + * Re-issue the user's self-binding doc with a new display name. Same shape + * as the doc created during onboarding (kind:"self"). Callers pair this + * with `deletePersonalBinding(oldId)` to drop the previous version — + * evault-core has no update mutation. + */ +export async function createSelfBindingDoc( + gqlUrl: string, + ownerEname: string, + ownerSignature: OwnerSignature, + name: string, +): Promise { + const subject = normalizeEname(ownerEname); + const result = await vaultGqlRequest( + gqlUrl, + ownerEname, + CREATE_BINDING_DOC_MUTATION, + { + input: { + subject, + type: "self", + data: { kind: "self", name }, + ownerSignature, + }, + }, + ); + return unwrapCreate(result); +} + // --------------------------------------------------------------------------- // Hash / validate // --------------------------------------------------------------------------- diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte index af4afd3da..f761019bc 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte @@ -19,6 +19,7 @@ let cachedEname: string | undefined; let cachedIsFake: boolean | undefined; let cachedLegalId: LegalIdDoc | null = null; let cachedDisplayName: string | undefined; +let cachedSelfDocId: string | undefined; let cachedSocialBindingCount = 0; let cachedSocialBindingPreview: SocialBindingDisplay[] = []; let hasEverLoaded = false; @@ -42,13 +43,19 @@ import { fetchSocialBindings, resolveVaultUri, } from "$lib/utils"; +import { getCanonicalBindingDocString } from "$lib/utils/bindingDocHash"; import { replaceAll as replaceAllPersonal } from "$lib/stores/personalBinding"; -import { loadPersonalBindings } from "$lib/utils/personalBinding"; +import { + createSelfBindingDoc, + deletePersonalBinding, + loadPersonalBindings, +} from "$lib/utils/personalBinding"; import { getContext, onDestroy, onMount, tick } from "svelte"; import { Shadow } from "svelte-loading-spinners"; import { fly } from "svelte/transition"; import AppsMarketplace from "./components/AppsMarketplace.svelte"; import BindingDocuments from "./components/BindingDocuments.svelte"; +import EditNameSheet from "./components/EditNameSheet.svelte"; import ENameCard from "./components/ENameCard.svelte"; import EVaultCard from "./components/EVaultCard.svelte"; import Greeting from "./components/Greeting.svelte"; @@ -93,6 +100,13 @@ let legalId = $state(cachedLegalId); // registration. Never gets overwritten by KYC the way userController.user.name // does, so this stays as the user's chosen handle on the home greeting. let displayName = $state(cachedDisplayName); +// MetaEnvelope id of the current self-binding doc. Captured at load so the +// edit flow can delete the old doc after writing a new one — evault-core has +// no update mutation, so the only way to "rename" is create-then-delete. +let selfDocId = $state(cachedSelfDocId); +let editNameOpen = $state(false); +let editNameSaving = $state(false); +let editNameError = $state(null); let kycOpen = $state(false); let eVaultInfoOpen = $state(false); let bindingDocsInfoOpen = $state(false); @@ -147,30 +161,36 @@ async function loadBindingDocuments(): Promise { body: JSON.stringify({ query: `query { bindingDocuments(first: 50) { - edges { node { parsed } } + edges { node { id parsed } } } }`, }), }); const json = await res.json(); - const edges: { node: { parsed: ParsedBindingDoc | null } }[] = - json?.data?.bindingDocuments?.edges ?? []; - - const parsedDocs = edges - .map((e) => e.node.parsed) - .filter((p): p is ParsedBindingDoc => p !== null); - - const idDoc = parsedDocs.find((p) => p.type === "id_document"); - legalId = idDoc ? toLegalIdDoc(idDoc) : null; + const edges: { + node: { id: string; parsed: ParsedBindingDoc | null }; + }[] = json?.data?.bindingDocuments?.edges ?? []; + + const docs = edges + .map((e) => ({ id: e.node.id, parsed: e.node.parsed })) + .filter( + (d): d is { id: string; parsed: ParsedBindingDoc } => + d.parsed !== null, + ); + + const idDocEntry = docs.find((d) => d.parsed.type === "id_document"); + legalId = idDocEntry ? toLegalIdDoc(idDocEntry.parsed) : null; cachedLegalId = legalId; - const selfDoc = parsedDocs.find((p) => p.type === "self"); - const selfName = selfDoc?.data?.name; + const selfDocEntry = docs.find((d) => d.parsed.type === "self"); + const selfName = selfDocEntry?.parsed.data?.name; if (typeof selfName === "string" && selfName.trim()) { displayName = selfName.trim(); cachedDisplayName = displayName; } + selfDocId = selfDocEntry?.id; + cachedSelfDocId = selfDocId; } catch (err) { console.warn("[main] Failed to load binding documents:", err); } @@ -178,6 +198,95 @@ async function loadBindingDocuments(): Promise { await loadSocialBindings(); } +// Edit the display name by writing a new self-binding doc and deleting the +// old one. evault-core exposes no update mutation, so this create+delete +// pair is the only path. If the delete fails we leave the orphan in place — +// the next load picks the most-recent self doc by timestamp anyway. +async function handleEditNameSave(newName: string): Promise { + if (!globalState) { + editNameError = "Wallet not ready."; + return; + } + const trimmed = newName.trim(); + if (!trimmed) { + editNameError = "Please enter a name."; + return; + } + if (trimmed === displayName) { + editNameOpen = false; + return; + } + + editNameSaving = true; + editNameError = null; + + try { + const vault = await globalState.vaultController.vault; + if (!vault?.uri || !vault?.ename) { + throw new Error("No eVault available"); + } + const ownerEname = vault.ename.startsWith("@") + ? vault.ename + : `@${vault.ename}`; + const gqlUrl = new URL("/graphql", vault.uri).toString(); + + const data = { kind: "self", name: trimmed }; + const canonical = getCanonicalBindingDocString({ + subject: ownerEname, + type: "self", + data, + }); + const signature = await globalState.keyService.sign(canonical); + const ownerSignature = { + signer: ownerEname, + signature, + timestamp: new Date().toISOString(), + }; + + const newId = await createSelfBindingDoc( + gqlUrl, + ownerEname, + ownerSignature, + trimmed, + ); + + const oldId = selfDocId; + displayName = trimmed; + cachedDisplayName = trimmed; + selfDocId = newId; + cachedSelfDocId = newId; + + // Mirror to userController so accordions/cards that still read from + // there stay consistent until their own loaders re-run. + if (userData) { + userData = { ...userData, name: trimmed }; + cachedUserData = userData; + globalState.userController.user = userData as Record; + } + + if (oldId && oldId !== newId) { + try { + await deletePersonalBinding(gqlUrl, ownerEname, oldId); + } catch (err) { + console.warn( + "[main] Couldn't delete old self-binding; load dedupe will handle it", + err, + ); + } + } + + editNameOpen = false; + } catch (err) { + console.error("[main] Edit name failed:", err); + editNameError = + err instanceof Error + ? err.message + : "Couldn't save your new name. Please try again."; + } finally { + editNameSaving = false; + } +} + // Hydrate the personalBinding store from the caller's vault so the // /main accordion reflects the user's marks on a cold reload — without // this they only appear after a round trip through /personal. @@ -741,6 +850,10 @@ async function refreshBindings(): Promise { name={displayName ?? (userData?.name as string) ?? ""} {notificationCount} {tourActive} + onedit={() => { + editNameError = null; + editNameOpen = true; + }} /> {/if} @@ -871,6 +984,14 @@ async function refreshBindings(): Promise { onbound={handleSocialBound} /> + + {#snippet body()} +import { ButtonAction } from "$lib/ui"; +import BottomSheet from "$lib/ui/BottomSheet/BottomSheet.svelte"; +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/svelte"; + +interface IEditNameSheetProps { + isOpen: boolean; + currentName?: string; + saving?: boolean; + error?: string | null; + onsave?: (name: string) => void; +} + +let { + isOpen = $bindable(), + currentName = "", + saving = false, + error = null, + onsave, +}: IEditNameSheetProps = $props(); + +let name = $state(""); + +// Seed the input from the prop whenever the sheet opens. Doing it in an +// effect rather than at script-eval time means the user's previous typing +// doesn't linger across opens. +$effect(() => { + if (!isOpen) return; + name = currentName; +}); + +const trimmed = $derived(name.trim()); +const canSave = $derived( + trimmed.length > 0 && trimmed !== currentName.trim() && !saving, +); + +function save() { + if (!canSave) return; + onsave?.(trimmed); +} + +function close() { + if (saving) return; + isOpen = false; +} + + + +
+

Edit name

+ +
+ +
+

+ This is the name shown on your home screen. Your legal name (from + your verified ID) and your eName are not affected. +

+ +
+ + +
+ + {#if error} + + {/if} + + + Save + +
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte index 2fc05fad9..20ee5f89e 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte @@ -30,11 +30,6 @@ const {

{name}

- {#if !tourActive}