diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 000000000..661611739
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": [
+ "Read(//c/Users/julie/AppData/Local/Packages/Claude_pzs8sxrjxfjjc/LocalCache/Roaming/Claude/local-agent-mode-sessions/184ceb9f-c63d-479f-9de2-eb3191328fca/d7f7f518-2e61-4301-ac45-f5c68b836ee3/local_1b897c7e-d73e-435f-a62d-ce4a750943aa/outputs/frames/**)"
+ ]
+ }
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 4e98aedf6..317f25aa8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -21,7 +21,7 @@
}
},
"[svelte]": {
- "editor.defaultFormatter": "svelte.svelte-vscode",
+ "editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
diff --git a/infrastructure/eid-wallet/package.json b/infrastructure/eid-wallet/package.json
index c9c7e5ca8..d4951b6be 100644
--- a/infrastructure/eid-wallet/package.json
+++ b/infrastructure/eid-wallet/package.json
@@ -22,21 +22,19 @@
"license": "MIT",
"dependencies": {
"@auvo/tauri-plugin-crypto-hw-api": "^0.1.0",
+ "@choochmeque/tauri-plugin-notifications-api": "^0.4.3",
"@didit-protocol/sdk-web": "^0.1.6",
+ "@fontsource-variable/roboto": "^5.2.10",
+ "@fontsource-variable/roboto-condensed": "^5.2.8",
"@hugeicons/core-free-icons": "^1.0.13",
"@hugeicons/svelte": "^1.0.2",
- "@iconify/svelte": "^5.0.1",
- "@ngneat/falso": "^7.3.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tauri-apps/api": "^2.9.0",
"@tauri-apps/plugin-barcode-scanner": "^2.4.2",
"@tauri-apps/plugin-biometric": "^2.3.2",
"@tauri-apps/plugin-deep-link": "^2.4.5",
- "@choochmeque/tauri-plugin-notifications-api": "^0.4.3",
"@tauri-apps/plugin-opener": "^2.5.2",
"@tauri-apps/plugin-store": "^2.4.1",
- "@veriff/incontext-sdk": "^2.4.0",
- "@veriff/js-sdk": "^1.5.1",
"axios": "^1.6.7",
"blindvote": "workspace:*",
"clsx": "^2.1.1",
@@ -44,12 +42,10 @@
"flag-icons": "^7.3.2",
"graphql-request": "^6.1.0",
"html5-qrcode": "^2.3.8",
- "import": "^0.0.6",
"jose": "^5.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-qrcode": "^1.0.1",
"tailwind-merge": "^3.0.2",
- "ts-md5": "^2.0.1",
"uuid": "^11.1.0",
"wallet-sdk": "workspace:*"
},
diff --git a/infrastructure/eid-wallet/src-tauri/Cargo.toml b/infrastructure/eid-wallet/src-tauri/Cargo.toml
index 790282b15..65f7866ae 100644
--- a/infrastructure/eid-wallet/src-tauri/Cargo.toml
+++ b/infrastructure/eid-wallet/src-tauri/Cargo.toml
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
-tauri = { version = "2", features = [] }
+tauri = { version = "2", features = ["devtools"] }
tauri-plugin-opener = "2"
tauri-plugin-deep-link = "2"
tauri-plugin-notifications = { version = "0.4", default-features = false, features = ["push-notifications", "notify-rust"] }
diff --git a/infrastructure/eid-wallet/src-tauri/capabilities/desktop.json b/infrastructure/eid-wallet/src-tauri/capabilities/desktop.json
new file mode 100644
index 000000000..d0147cab7
--- /dev/null
+++ b/infrastructure/eid-wallet/src-tauri/capabilities/desktop.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "desktop-capability",
+ "description": "Capability for the main window on desktop",
+ "windows": [
+ "main"
+ ],
+ "permissions": [
+ "core:default",
+ "opener:default",
+ "store:default",
+ "deep-link:default",
+ "notifications:default",
+ "process:default",
+ "opener:allow-default-urls"
+ ],
+ "platforms": [
+ "windows",
+ "macOS",
+ "linux"
+ ]
+}
diff --git a/infrastructure/eid-wallet/src-tauri/tauri.conf.json b/infrastructure/eid-wallet/src-tauri/tauri.conf.json
index ab31af488..2ba504829 100644
--- a/infrastructure/eid-wallet/src-tauri/tauri.conf.json
+++ b/infrastructure/eid-wallet/src-tauri/tauri.conf.json
@@ -19,7 +19,8 @@
],
"security": {
"capabilities": [
- "mobile-capability"
+ "mobile-capability",
+ "desktop-capability"
],
"csp": null
}
diff --git a/infrastructure/eid-wallet/src/app.css b/infrastructure/eid-wallet/src/app.css
index 7df806969..756bcdc9c 100644
--- a/infrastructure/eid-wallet/src/app.css
+++ b/infrastructure/eid-wallet/src/app.css
@@ -1,12 +1,7 @@
@import "tailwindcss";
@import "flag-icons/css/flag-icons.min.css";
-
-@font-face {
- font-family: "Archivo";
- src: url("/fonts/Archivo-VariableFont_wdth,wght.ttf") format("truetype");
- font-weight: 100 900;
- font-style: normal;
-}
+@import "@fontsource-variable/roboto";
+@import "@fontsource-variable/roboto-condensed";
@layer base {
/* Typography */
@@ -15,34 +10,34 @@
}
h2 {
- @apply text-6xl/[1.5] text-black font-semibold;
+ @apply text-6xl/normal text-black font-semibold;
}
h3 {
- @apply text-3xl/[1.5] text-black font-semibold;
+ @apply text-3xl/normal text-black font-semibold;
}
h4 {
- @apply text-xl/[1.5] text-black font-semibold;
+ @apply text-xl/normal text-black font-semibold;
}
p {
- @apply text-base/[1.5] text-black font-normal;
+ @apply text-base/normal text-black font-normal;
}
.small {
- @apply text-xs/[1.5] text-black font-normal;
+ @apply text-xs/normal text-black font-normal;
}
}
@theme {
/* Custom theme */
- --color-primary: #8e52ff;
- --color-primary-100: #e8dcff;
- --color-primary-200: #d2baff;
- --color-primary-300: #bb97ff;
- --color-primary-400: #a575ff;
- --color-primary-500: #8e52ff;
+ --color-primary: #8968ff;
+ --color-primary-100: #e7e1ff;
+ --color-primary-200: #d0c3ff;
+ --color-primary-300: #b8a4ff;
+ --color-primary-400: #a186ff;
+ --color-primary-500: #8968ff;
--color-secondary: #73efd5;
--color-secondary-100: #e3fcf7;
@@ -54,12 +49,12 @@
--color-white: #ffffff;
--color-gray: #f5f5f5;
- --color-black: #1f1f1f;
- --color-black-100: #d2d2d2;
- --color-black-300: #a5a5a5;
- --color-black-500: #797979;
- --color-black-700: #4c4c4c;
- --color-black-900: #1f1f1f;
+ --color-black: #1d2636;
+ --color-black-100: #dce0e8;
+ --color-black-300: #b8c0cb;
+ --color-black-500: #788292;
+ --color-black-700: #364052;
+ --color-black-900: #1d2636;
--color-danger: #ff5255;
--color-danger-100: #ffdcdd;
@@ -67,28 +62,22 @@
--color-danger-300: #ff968e;
--color-danger-400: #ff7b77;
--color-danger-500: #ff5255;
+
+ /* Typography */
+ --font-sans: "Roboto Variable", sans-serif;
+ --font-condensed: "Roboto Condensed Variable", sans-serif;
}
body {
- font-family: "Archivo", sans-serif;
- /* padding-top: env(safe-area-inset-top); */
- /* padding-bottom: env(safe-area-inset-bottom); */
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
- background-color: var(--color-primary);
}
/* Ensure background remains correct during transitions */
:root[data-transition]::view-transition-group(root),
:root[data-transition]::view-transition-old(root),
:root[data-transition]::view-transition-new(root) {
- background-color: white !important; /* Default to white */
-}
-
-.dark:root[data-transition]::view-transition-group(root),
-.dark:root[data-transition]::view-transition-old(root),
-.dark:root[data-transition]::view-transition-new(root) {
- background-color: #0b0d13 !important; /* Use dark background in dark mode */
+ background-color: white !important;
}
/* Prevent flickering */
@@ -155,18 +144,24 @@ body {
}
}
-:root[data-transition]::view-transition-old(root) {
- animation: 400ms ease-out both fade-out;
+/* Forward (data-transition="left"): the NEW page slides in from the right,
+ covering the OLD which stays put. */
+:root[data-transition="left"]::view-transition-old(root) {
+ animation: none;
}
-
-:root[data-transition="right"]::view-transition-new(root) {
+:root[data-transition="left"]::view-transition-new(root) {
animation: 200ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
position: relative;
z-index: 1;
}
-:root[data-transition="left"]::view-transition-new(root) {
- animation: 200ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-left;
+/* Backward (data-transition="right"): the OLD page slides out to the right,
+ revealing the NEW page underneath which stays put. */
+:root[data-transition="right"]::view-transition-new(root) {
+ animation: none;
+}
+:root[data-transition="right"]::view-transition-old(root) {
+ animation: 200ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
position: relative;
z-index: 1;
}
diff --git a/infrastructure/eid-wallet/src/app.html b/infrastructure/eid-wallet/src/app.html
index 02490b24f..e1f2955cf 100644
--- a/infrastructure/eid-wallet/src/app.html
+++ b/infrastructure/eid-wallet/src/app.html
@@ -4,7 +4,6 @@
-
Tauri + SvelteKit + Typescript App
%sveltekit.head%
diff --git a/infrastructure/eid-wallet/src/lib/fragments/SplashScreen/SplashScreen.svelte b/infrastructure/eid-wallet/src/lib/fragments/SplashScreen/SplashScreen.svelte
index ac5317a25..8162be5a6 100644
--- a/infrastructure/eid-wallet/src/lib/fragments/SplashScreen/SplashScreen.svelte
+++ b/infrastructure/eid-wallet/src/lib/fragments/SplashScreen/SplashScreen.svelte
@@ -1,62 +1,126 @@
-
-

-

+
+
+
+
+ eID
+
+
+
+
+ Your Digital Self
+
+
+

+
+
+
+ {#if showDrawer}
-

-
+ {/if}
diff --git a/infrastructure/eid-wallet/src/lib/global/state.ts b/infrastructure/eid-wallet/src/lib/global/state.ts
index 3dc2587f3..cc6481730 100644
--- a/infrastructure/eid-wallet/src/lib/global/state.ts
+++ b/infrastructure/eid-wallet/src/lib/global/state.ts
@@ -166,6 +166,26 @@ export class GlobalState {
}
}
+ get hasSeenWelcomeTour() {
+ return this.#store
+ .get("hasSeenWelcomeTour")
+ .then((value) => value ?? false)
+ .catch((error) => {
+ console.error("Failed to get welcome-tour status:", error);
+ return false;
+ });
+ }
+
+ set hasSeenWelcomeTour(value: boolean | Promise) {
+ const resolve =
+ value instanceof Promise ? value : Promise.resolve(value);
+ resolve
+ .then((resolved) => this.#store.set("hasSeenWelcomeTour", resolved))
+ .catch((error) => {
+ console.error("Failed to set welcome-tour status:", error);
+ });
+ }
+
async reset() {
try {
await this.securityController.clear();
@@ -174,6 +194,7 @@ export class GlobalState {
await this.keyService.clear();
await this.#store.delete("initialized");
await this.#store.delete("isOnboardingComplete");
+ await this.#store.delete("hasSeenWelcomeTour");
} catch (error) {
console.error("Failed to reset global state:", error);
}
diff --git a/infrastructure/eid-wallet/src/lib/ui/Button/ButtonIcon.svelte b/infrastructure/eid-wallet/src/lib/ui/Button/ButtonIcon.svelte
index 45b569bf4..7b147b181 100644
--- a/infrastructure/eid-wallet/src/lib/ui/Button/ButtonIcon.svelte
+++ b/infrastructure/eid-wallet/src/lib/ui/Button/ButtonIcon.svelte
@@ -93,23 +93,25 @@ const textColor: Record = {
danger: "text-danger",
} as const;
-const resolvedIconSize =
+const resolvedIconSize = $derived(
iconSize === undefined
? iconSizeVariant.md
: typeof iconSize === "number"
? iconSize
: iconSize in iconSizeVariant
? iconSizeVariant[iconSize as keyof typeof iconSizeVariant]
- : iconSize;
+ : iconSize,
+);
-const resolvedBgSize =
+const resolvedBgSize = $derived(
bgSize === undefined
? "" // if bgSize is empty, there is no background
: typeof bgSize === "number"
? `h-${bgSize} w-${bgSize}`
: bgSize in sizeVariant
? sizeVariant[bgSize as keyof typeof sizeVariant]
- : bgSize;
+ : bgSize,
+);
const classes = $derived({
common: cn(
diff --git a/infrastructure/eid-wallet/src/routes/(app)/+layout.svelte b/infrastructure/eid-wallet/src/routes/(app)/+layout.svelte
index adf61f5d3..d04f00a2c 100644
--- a/infrastructure/eid-wallet/src/routes/(app)/+layout.svelte
+++ b/infrastructure/eid-wallet/src/routes/(app)/+layout.svelte
@@ -14,8 +14,16 @@ let globalState: GlobalState | undefined = $state(undefined);
let notificationListener: PluginListener | undefined;
onMount(async () => {
- // Get global state
- globalState = getContext<() => GlobalState>("globalState")();
+ // Get global state — poll briefly since root layout's init is async and
+ // can land after this guard mounts on a hard reload.
+ const getGlobalState = getContext<() => GlobalState>("globalState");
+ globalState = getGlobalState();
+ let retries = 0;
+ while (!globalState && retries < 50) {
+ await new Promise((r) => setTimeout(r, 100));
+ globalState = getGlobalState();
+ retries++;
+ }
// Authentication guard for all app routes
try {
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte
index 260b429b5..362c9e02d 100644
--- a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte
@@ -1,31 +1,25 @@
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/BindingDocuments.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/BindingDocuments.svelte
new file mode 100644
index 000000000..c6182902d
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/BindingDocuments.svelte
@@ -0,0 +1,119 @@
+
+
+
+
+ Binding Documents
+
+
+
+
+
+
+
+
+
+ Legal ID
+
+
+ Any legal doc
+
+
+
+
+
+
+
+
+
+
+
+ Personal
+
+
+ Idenity marks
+
+
+
+
+
+
+
+
+
+
+
+ Social binding
+
+
+ New level of trust
+
+
+
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/ENameCard.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/ENameCard.svelte
new file mode 100644
index 000000000..fbedf5780
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/ENameCard.svelte
@@ -0,0 +1,89 @@
+
+
+
+
+
Your eName
+
+ Unverified ID
+
+
+
+
+ {ename ?? "Loading..."}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Share your eName
+
+ Anyone scanning this can see your eName
+
+
+
+ Share
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/EVaultCard.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/EVaultCard.svelte
new file mode 100644
index 000000000..113bb0342
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/EVaultCard.svelte
@@ -0,0 +1,36 @@
+
+
+
+
+
Your eVault
+
+ {available}
+ available
+
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte
new file mode 100644
index 000000000..d3b184a1c
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/Greeting.svelte
@@ -0,0 +1,69 @@
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/Lasso.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/Lasso.svelte
new file mode 100644
index 000000000..2fe7a521d
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/Lasso.svelte
@@ -0,0 +1,80 @@
+
+
+
+{#if active}
+
+{/if}
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/ScanFAB.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/ScanFAB.svelte
new file mode 100644
index 000000000..cea021794
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/ScanFAB.svelte
@@ -0,0 +1,31 @@
+
+
+
+
+ Scan
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/components/WelcomeTour.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/components/WelcomeTour.svelte
new file mode 100644
index 000000000..e1b26c7cc
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/components/WelcomeTour.svelte
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+ e.preventDefault()}
+ ontouchmove={(e) => e.preventDefault()}
+>
+
+
+
+ {#key step}
+
+ {def.description}
+
+ {/key}
+
+
+ {def.cta}
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/legacy/EPassportSection.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/legacy/EPassportSection.svelte
new file mode 100644
index 000000000..ee4d4bdf5
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/legacy/EPassportSection.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+ ePassport
+
+ goto("/ePassport")}
+ role="link"
+ tabindex="0"
+ onkeydown={(e) => {
+ if (e.key === "Enter") goto("/ePassport");
+ }}
+ >
+
+
+
+ {#if bindingDocsLoaded && (hasOnlySelfDocs || missingProvisionerDocs)}
+
+ {/if}
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/legacy/KycUpgradeOverlay.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/legacy/KycUpgradeOverlay.svelte
new file mode 100644
index 000000000..47c7930d3
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/legacy/KycUpgradeOverlay.svelte
@@ -0,0 +1,535 @@
+
+
+
+{#if kycStep !== "idle"}
+ {#if kycStep === "checking-hw" || kycStep === "hw-error" || kycStep === "starting" || kycStep === "upgrading"}
+
+
+
+
+
+ {#if kycError}
+
+ {kycError}
+
+ {/if}
+
+ {#if kycStep === "checking-hw" || kycStep === "starting" || kycStep === "upgrading"}
+
+
+
+ {kycStep === "checking-hw"
+ ? "Checking device capabilities..."
+ : kycStep === "upgrading"
+ ? "Upgrading your eVault…"
+ : "Starting verification…"}
+
+
+ {:else if kycStep === "hw-error"}
+
+ Hardware Security Not Available
+
+
+ Your phone doesn't support hardware crypto keys,
+ which is a requirement for verified IDs.
+
+
+ Hardware-backed identity verification is not
+ available on this device.
+
+ {/if}
+
+
+ {#if kycStep === "hw-error"}
+
+
+ Cancel
+
+
+ {/if}
+
+
+ {/if}
+
+ {#if kycStep === "verifying"}
+
+ {/if}
+
+ {#if kycStep === "duplicate"}
+
+
+
+
+ !
+
+
Identity Already Registered
+
+
+ This identity document is already linked to an existing eVault.
+ You can't create a duplicate — each person gets one verified
+ eVault.
+
+ {#if duplicateEName}
+
+
+ Your existing eVault eName
+
+
+ {duplicateEName}
+
+
+
+ Use the eName above to recover access to your existing
+ eVault instead.
+
+ {/if}
+
+
+ Got it
+
+
+
+ {/if}
+
+ {#if kycStep === "result"}
+
+
+ {#if kycError}
+
+ {kycError}
+
+ {/if}
+
+ {#if diditResult === "approved"}
+
+
+ ✓
+
+
Identity Verified
+
+
+ Your identity has been verified. Your eVault trust level
+ will now be upgraded.
+
+
+
+ Continue
+
+
+ {:else if diditResult === "in_review"}
+
+
+ Your verification is being manually reviewed. You'll be
+ notified when it's complete.
+
+
+
+ Close
+
+
+ {:else}
+
+
+ ✗
+
+
Verification Failed
+
+
+ {diditRejectionReason ??
+ "Your verification could not be completed."}
+
+
+
+ Try Again
+
+
+ Cancel
+
+
+ {/if}
+
+ {/if}
+{/if}
diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/legacy/MarketplaceBanner.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/legacy/MarketplaceBanner.svelte
new file mode 100644
index 000000000..a6fcea346
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(app)/main/legacy/MarketplaceBanner.svelte
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+ Discover Post Platforms
+
+
+ Explore
+
+

+ -enabled services
+
+
+
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte
index 215a6df87..bbb7d08f4 100644
--- a/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte
+++ b/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte
@@ -19,7 +19,7 @@ import { onDestroy } from "svelte";
const getGlobalState = getContext<() => GlobalState>("globalState");
const setGlobalState =
getContext<(value: GlobalState) => void>("setGlobalState");
-let globalState = getGlobalState();
+let globalState = $derived(getGlobalState());
let isDeleteConfirmationOpen = $state(false);
let isFinalConfirmationOpen = $state(false);
@@ -42,10 +42,13 @@ function confirmDelete() {
async function nukeWallet() {
clearAllNotifications();
+ if (!globalState) {
+ console.error("Cannot nuke wallet: global state not ready");
+ return;
+ }
const newGlobalState = await globalState.reset();
setGlobalState(newGlobalState);
- globalState = newGlobalState;
- goto("/onboarding");
+ goto("/");
}
async function cancelDelete() {
diff --git a/infrastructure/eid-wallet/src/routes/(auth)/+layout.svelte b/infrastructure/eid-wallet/src/routes/(auth)/+layout.svelte
index e1beffc92..bbbc612f4 100644
--- a/infrastructure/eid-wallet/src/routes/(auth)/+layout.svelte
+++ b/infrastructure/eid-wallet/src/routes/(auth)/+layout.svelte
@@ -13,7 +13,16 @@ const getGlobalState = getContext<() => GlobalState>("globalState");
onMount(async () => {
try {
- const globalState = getGlobalState();
+ // Root layout init is async — on a hard reload directly into an
+ // (auth) route, this guard can mount before globalState is set.
+ // Poll briefly instead of failing immediately.
+ let globalState = getGlobalState();
+ let retries = 0;
+ while (!globalState && retries < 50) {
+ await new Promise((r) => setTimeout(r, 100));
+ globalState = getGlobalState();
+ retries++;
+ }
if (!globalState) {
console.error("Global state is not defined");
guardFailed = true;
diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte
index 27e33f92c..c5c6bf77b 100644
--- a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte
+++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte
@@ -13,15 +13,22 @@ import { ButtonAction } from "$lib/ui";
import { capitalize, getCanonicalBindingDocString } from "$lib/utils";
import axios from "axios";
import { GraphQLClient } from "graphql-request";
-import { getContext, onMount } from "svelte";
+import { getContext, onMount, tick } from "svelte";
+import { cubicOut } from "svelte/easing";
import { Shadow } from "svelte-loading-spinners";
import { v4 as uuidv4 } from "uuid";
import { provision } from "wallet-sdk";
+import NameInput from "./steps/NameInput.svelte";
+import PinCreate from "./steps/PinCreate.svelte";
+import PinRepeat from "./steps/PinRepeat.svelte";
const ANONYMOUS_VERIFICATION_CODE = "d66b7138-538a-465f-a6ce-f6985854c3f4";
const KEY_ID = "default";
type Step =
+ | "pin-create"
+ | "pin-repeat"
+ | "name"
| "home"
| "new-evault"
| "already-have"
@@ -69,10 +76,205 @@ interface DiditCompleteResult {
};
}
-let step = $state("home");
+let step = $state("pin-create");
let error = $state(null);
let loading = $state(false);
+// New-onboarding flow state (PIN create -> repeat -> name -> new-evault)
+let pinFirstAttempt = $state("");
+let chosenName = $state("");
+let nameError = $state(null);
+
+const handlePinCreateBack = () => {
+ // Skip the splash A→B intro on the way back — the route slide is the
+ // visible transition, replaying the logo animation on top would clash.
+ sessionStorage.setItem("splashImmediate", "true");
+ goto("/");
+};
+
+// Tracks which way the next step swap should slide.
+let stepDirection = $state<"forward" | "backward">("forward");
+
+const goToStep = (nextStep: Step, direction: "forward" | "backward") => {
+ stepDirection = direction;
+ step = nextStep;
+};
+
+// Asymmetric step transitions: only the active element moves.
+// Forward → NEW slides in over OLD (OLD stays put).
+// Backward → OLD slides out to the right, revealing NEW (NEW stays put).
+function slideIn(
+ _node: HTMLElement,
+ { direction }: { direction: "forward" | "backward" },
+) {
+ if (direction === "backward") {
+ return { duration: 0 };
+ }
+ return {
+ duration: 200,
+ easing: cubicOut,
+ css: (t: number) =>
+ `transform: translateX(${(1 - t) * 100}%); z-index: 70;`,
+ };
+}
+
+function slideOut(
+ _node: HTMLElement,
+ { direction }: { direction: "forward" | "backward" },
+) {
+ if (direction === "forward") {
+ return {
+ duration: 200,
+ css: () => `transform: translateX(0);`,
+ };
+ }
+ return {
+ duration: 200,
+ easing: cubicOut,
+ css: (t: number) =>
+ `transform: translateX(${(1 - t) * 100}%); z-index: 70;`,
+ };
+}
+
+const handlePinCreateComplete = (enteredPin: string) => {
+ pinFirstAttempt = enteredPin;
+ goToStep("pin-repeat", "forward");
+};
+
+const handlePinRepeatBack = () => {
+ pinFirstAttempt = "";
+ goToStep("pin-create", "backward");
+};
+
+const handlePinRepeatComplete = async (confirmedPin: string) => {
+ // Persist the PIN now — local hash via Rust, no network. Both attempts are
+ // already validated as matching by PinRepeat, but setOnboardingPin double-
+ // checks. If hashing/storing fails, propagate to PinRepeat so the user
+ // sees an error instead of silently advancing.
+ await globalState.securityController.setOnboardingPin(
+ pinFirstAttempt,
+ confirmedPin,
+ );
+ goToStep("name", "forward");
+};
+
+const handleNameBack = () => {
+ nameError = null;
+ goToStep("pin-repeat", "backward");
+};
+
+const handleNameComplete = async (enteredName: string) => {
+ chosenName = enteredName;
+ nameError = null;
+ // Drop straight into the legacy loading screen — Shadow spinner + status.
+ step = "loading";
+
+ try {
+ globalState.userController.isFake = true;
+ await globalState.walletSdkAdapter.ensureKey(KEY_ID, "onboarding");
+
+ const provisionResult = await provision(globalState.walletSdkAdapter, {
+ registryUrl: PUBLIC_REGISTRY_URL,
+ provisionerUrl: PUBLIC_PROVISIONER_URL,
+ namespace: uuidv4(),
+ verificationId: ANONYMOUS_VERIFICATION_CODE,
+ keyId: KEY_ID,
+ context: "onboarding",
+ isPreVerification: true,
+ });
+
+ if (provisionResult.duplicate) {
+ throw new Error("An eVault already exists for this identity.");
+ }
+ if (
+ !provisionResult.success ||
+ !provisionResult.w3id ||
+ !provisionResult.uri
+ ) {
+ throw new Error("Provisioning failed — incomplete response.");
+ }
+
+ const ename = provisionResult.w3id;
+ const uri = provisionResult.uri;
+
+ // Resolve eVault GraphQL endpoint from registry.
+ const resolveResp = await axios.get(
+ new URL(`resolve?w3id=${ename}`, PUBLIC_REGISTRY_URL).toString(),
+ );
+ const evaultUri = resolveResp.data.uri as string;
+ const graphqlEndpoint = new URL("/graphql", evaultUri).toString();
+
+ // Sign the self-declaration binding document.
+ const timestamp = new Date().toISOString();
+ const subject = ename.startsWith("@") ? ename : `@${ename}`;
+ const bindingData = { kind: "self", name: enteredName };
+ const payload = getCanonicalBindingDocString({
+ subject,
+ type: "self",
+ data: bindingData,
+ });
+ const signature = await globalState.walletSdkAdapter.signPayload(
+ KEY_ID,
+ "signing",
+ payload,
+ );
+
+ const gqlClient = new GraphQLClient(graphqlEndpoint, {
+ headers: {
+ "X-ENAME": ename,
+ ...(PUBLIC_EID_WALLET_TOKEN
+ ? { Authorization: `Bearer ${PUBLIC_EID_WALLET_TOKEN}` }
+ : {}),
+ },
+ });
+
+ const bdResult = await gqlClient.request<{
+ createBindingDocument: {
+ metaEnvelopeId: string | null;
+ errors: { message: string; code: string }[] | null;
+ };
+ }>(
+ `mutation CreateBindingDocument($input: CreateBindingDocumentInput!) {
+ createBindingDocument(input: $input) {
+ metaEnvelopeId
+ errors { message code }
+ }
+ }`,
+ {
+ input: {
+ subject,
+ type: "self",
+ data: bindingData,
+ ownerSignature: {
+ signer: subject,
+ signature,
+ timestamp,
+ },
+ },
+ },
+ );
+
+ const bdErrors = bdResult.createBindingDocument.errors;
+ if (bdErrors?.length) {
+ throw new Error(`Binding document error: ${bdErrors[0].message}`);
+ }
+
+ // Persist user + vault, then mark onboarding done.
+ globalState.userController.user = { name: enteredName };
+ globalState.vaultController.vault = { uri, ename };
+ globalState.isOnboardingComplete = true;
+
+ // TODO(next): step = "welcome" once the Welcome screen is built.
+ // For now we land directly on /main.
+ await goto("/main");
+ } catch (err) {
+ console.error("Failed to provision eVault:", err);
+ nameError =
+ "Couldn't create your eVault. Check your connection and try again.";
+ step = "name";
+ }
+};
+
// KYC panel sub-state
let checkingHardware = $state(false);
let showHardwareError = $state(false);
@@ -731,9 +933,26 @@ const handleEnamePassphraseRecovery = async () => {
};
onMount(async () => {
- // Detect upgrade mode from query param
+ // Refresh guard: if the user landed here without going through the splash
+ // CTA (i.e. a hard reload or external nav), bounce back to /. Step state
+ // is in-memory only, so resuming mid-flow on refresh would drop them on
+ // pin-create regardless of how far they got — better to restart cleanly.
+ // The flag is set by splash's "Create Digital Self" handler and cleared
+ // here on first read.
const url = new URL(window.location.href);
- if (url.searchParams.get("upgrade") === "1") {
+ const upgradeRequested = url.searchParams.get("upgrade") === "1";
+ if (!upgradeRequested) {
+ const fromSplash =
+ sessionStorage.getItem("navigatingToOnboarding") === "true";
+ sessionStorage.removeItem("navigatingToOnboarding");
+ if (!fromSplash) {
+ await goto("/");
+ return;
+ }
+ }
+
+ // Detect upgrade mode from query param
+ if (upgradeRequested) {
upgradeMode = true;
step = "kyc-panel";
// Trigger hardware check immediately
@@ -742,6 +961,36 @@ onMount(async () => {
});
+{#if step === "pin-create" || step === "pin-repeat" || step === "name"}
+
+ {#key step}
+
+ {#if step === "pin-create"}
+
+ {:else if step === "pin-repeat"}
+
+ {:else if step === "name"}
+
+ {/if}
+
+ {/key}
+
+{:else}
{
{/if}
+{/if}
diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/NameInput.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/NameInput.svelte
new file mode 100644
index 000000000..931c0dbe4
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/NameInput.svelte
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/PinCreate.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/PinCreate.svelte
new file mode 100644
index 000000000..a7c40cd28
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/PinCreate.svelte
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/PinRepeat.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/PinRepeat.svelte
new file mode 100644
index 000000000..b27b1db82
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/PinRepeat.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/StepHeader.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/StepHeader.svelte
new file mode 100644
index 000000000..f43063030
--- /dev/null
+++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/steps/StepHeader.svelte
@@ -0,0 +1,39 @@
+
+
+
diff --git a/infrastructure/eid-wallet/src/routes/+layout.svelte b/infrastructure/eid-wallet/src/routes/+layout.svelte
index 39ea4ce33..9cd5361fd 100644
--- a/infrastructure/eid-wallet/src/routes/+layout.svelte
+++ b/infrastructure/eid-wallet/src/routes/+layout.svelte
@@ -1,8 +1,9 @@
-{#if showSplashScreen}
-
-{:else}
-
-
- {#if children}
- {@render children()}
- {/if}
-
-{/if}
+
+
+ {#if children}
+ {#key page.url.pathname}
+
+ {@render children()}
+
+ {/key}
+ {/if}
+
+
+