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 @@
-
- illustration - illustration + +
+
+ + eID + + +
+

+ Your Digital Self +

+
+ W3DS +
+
+ + {#if showDrawer}
-

- eID Wallet -

-

- for Web 3.0 Data Space + Create Digital Self + + + Restore Digital Self + +

+ By continuing you agree to our + + Terms & Conditions + + and + + Privacy Policy +

- logo -
+ {/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 @@ + +
+ +

Apps marketplace

+ +
+
+
+
+ B +
+

Blasby

+

Social

+
+
+
+ P +
+

Pictique

+

Social

+
+
+
+ E +
+

eVoting

+

Governance

+
+
+
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 @@ + + +
+
+

+ {greeting} +

+
+

+ {name} +

+ +
+
+ +
+ 0 + ? `Notifications (${notificationCount} unread)` + : "Notifications"} + > + + {#if notificationCount > 0} + + {notificationCount > 99 ? "99+" : notificationCount} + + {/if} + + + + +
+
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 @@ + + + + + + + + + + +
+ {#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"} +
+
+
+ get-started + + {#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"} +
+
+ ⏳ +
+

Under 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 @@ + + + + + Marketplace +
+ + Discover Post Platforms + + + Explore +
+ W3DS Logo + -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 @@ + + +
+ + +
+ + + {#if error} + + {/if} +
+ +
+ + Next + +
+
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 @@ + + +
+ + +
+ + + +
+ +
+ + Next + +
+
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 @@ + + +
+ + +
+ + + {#if error} + + {/if} + + +
+ +
+ + Next + +
+
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 @@ + + +
+ +
+

{title}

+

+ {step} of {total} steps +

+
+
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} +
+ +