Skip to content

Commit dfeab97

Browse files
committed
feat: KYC-First Onboarding
# Conflicts: # infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte # infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte # infrastructure/eid-wallet/src/routes/(auth)/verify/+page.svelte
1 parent 46c1b59 commit dfeab97

File tree

15 files changed

+1173
-484
lines changed

15 files changed

+1173
-484
lines changed

infrastructure/eid-wallet/src/lib/fragments/IdentityCard/IdentityCard.svelte

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ const baseClasses = `relative ${variant === "eName" ? "bg-black-900" : variant =
8585
class="bg-white text-black flex items-center leading-0 justify-center rounded-full h-7 px-5 text-xs font-medium"
8686
>
8787
{#if userData}
88-
{userData.isFake ? "DEMO ID" : "VERIFIED ID"}
88+
{#if userData.assuranceLevel === "KYC_VERIFIED"}
89+
VERIFIED ID
90+
{:else if userData.isFake}
91+
DEMO ID
92+
{:else}
93+
UNVERIFIED
94+
{/if}
8995
{/if}
9096
</p>
9197
{#if viewBtn}
@@ -97,7 +103,7 @@ const baseClasses = `relative ${variant === "eName" ? "bg-black-900" : variant =
97103
/>
98104
{/if}
99105
{:else if variant === "eVault"}
100-
<h3 class="text-black-300 text-3xl font-semibold mb-1 z-[1]">
106+
<h3 class="text-black-300 text-3xl font-semibold mb-1 z-1">
101107
{state.progressWidth} Used
102108
</h3>
103109
{/if}
@@ -111,7 +117,7 @@ const baseClasses = `relative ${variant === "eName" ? "bg-black-900" : variant =
111117
{:else if variant === "ePassport"}
112118
<div class="flex gap-2 flex-col">
113119
{#if userData}
114-
{#each Object.entries(userData).filter(([f, v]) => f !== "isFake") as [fieldName, value]}
120+
{#each Object.entries(userData).filter(([f, v]) => f !== "isFake" && f !== "assuranceLevel") as [fieldName, value]}
115121
<div class="flex justify-between">
116122
<p class="text-gray capitalize">{fieldName}</p>
117123
<p class=" font-medium text-white">{value}</p>
@@ -122,8 +128,8 @@ const baseClasses = `relative ${variant === "eName" ? "bg-black-900" : variant =
122128
{:else if variant === "eVault"}
123129
<div>
124130
<div class="flex justify-between mb-1">
125-
<p class="z-[1]">{usedStorage}GB Used</p>
126-
<p class="z-[1]">{totalStorage}GB total storage</p>
131+
<p class="z-1">{usedStorage}GB Used</p>
132+
<p class="z-1">{totalStorage}GB total storage</p>
127133
</div>
128134
<div
129135
class="relative w-full h-3 rounded-full overflow-hidden bg-primary-400"

infrastructure/eid-wallet/src/lib/global/controllers/evault.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export class VaultController {
5757
#walletSdkAdapter: import("wallet-sdk").CryptoAdapter | null = null;
5858
#profileCreationStatus: "idle" | "loading" | "success" | "failed" = "idle";
5959
#notificationService: NotificationService;
60+
#demoMode = false;
6061

6162
constructor(
6263
store: Store,
@@ -71,6 +72,21 @@ export class VaultController {
7172
this.#notificationService = NotificationService.getInstance();
7273
}
7374

75+
/**
76+
* Enable/disable demo mode
77+
* When enabled, skips network operations (sync, notifications, profile creation)
78+
*/
79+
set demoMode(value: boolean) {
80+
this.#demoMode = value;
81+
if (value) {
82+
console.log("🎭 VaultController: Demo mode enabled - skipping network operations");
83+
}
84+
}
85+
86+
get demoMode() {
87+
return this.#demoMode;
88+
}
89+
7490
/**
7591
* Get the current profile creation status
7692
*/
@@ -397,6 +413,13 @@ export class VaultController {
397413
else if (vault?.ename) {
398414
this.#store.set("vault", vault);
399415

416+
// Skip network operations in demo mode
417+
if (this.#demoMode) {
418+
console.log("🎭 Demo mode: Skipping network operations for vault", vault.ename);
419+
this.profileCreationStatus = "success";
420+
return;
421+
}
422+
400423
// Register device for notifications
401424
this.registerDeviceForNotifications(vault.ename);
402425

@@ -468,6 +491,97 @@ export class VaultController {
468491
return this.#endpoint;
469492
}
470493

494+
/**
495+
* Create a binding document in the eVault
496+
* TODO: Implement when backend endpoint is ready
497+
*
498+
* @param type - Type of binding document (PHYSICAL_ID, PASSPHRASE, PHOTOGRAPH, FRIEND)
499+
* @param data - Binding document data
500+
* @param signature - Signature or JWT proving authenticity
501+
*/
502+
async createBindingDocument(
503+
type: string,
504+
data: Record<string, unknown>,
505+
signature: string
506+
): Promise<void> {
507+
console.log("TODO: Create binding document", {
508+
type,
509+
data,
510+
signature: signature.substring(0, 50) + "...",
511+
});
512+
513+
// Emit audit event
514+
this.emitAuditEvent("BINDING_DOCUMENT_CREATED", {
515+
type,
516+
timestamp: new Date().toISOString(),
517+
});
518+
519+
// TODO: Call GraphQL mutation when backend is ready
520+
// const CREATE_BINDING_DOC = `
521+
// mutation CreateBindingDocument($input: BindingDocumentInput!) {
522+
// createBindingDocument(input: $input) {
523+
// id
524+
// type
525+
// status
526+
// }
527+
// }
528+
// `;
529+
}
530+
531+
/**
532+
* Request ePassport certificate from Remote CA
533+
* TODO: Implement when CA endpoint is ready
534+
*
535+
* @param ename - User's eName
536+
* @param publicKey - User's public key
537+
*/
538+
async requestEPassport(ename: string, publicKey: string): Promise<string> {
539+
console.log("TODO: Request ePassport from CA", {
540+
ename,
541+
publicKey: publicKey.substring(0, 50) + "...",
542+
});
543+
544+
// Emit audit event
545+
this.emitAuditEvent("EPASSPORT_REQUESTED", {
546+
ename,
547+
timestamp: new Date().toISOString(),
548+
});
549+
550+
// TODO: Call CA endpoint when ready
551+
// const response = await axios.post(
552+
// new URL("/ca/issue-epassport", PUBLIC_REGISTRY_URL).toString(),
553+
// { ename, publicKey }
554+
// );
555+
// return response.data.certificate;
556+
557+
return "TODO_EPASSPORT_CERTIFICATE";
558+
}
559+
560+
/**
561+
* Emit an audit event
562+
* These events are logged for compliance and debugging
563+
*
564+
* @param eventType - Type of audit event
565+
* @param data - Event data
566+
*/
567+
emitAuditEvent(eventType: string, data: Record<string, unknown>): void {
568+
const auditEvent = {
569+
type: eventType,
570+
timestamp: new Date().toISOString(),
571+
...data,
572+
};
573+
574+
console.log("AUDIT:", auditEvent);
575+
576+
// TODO: Send to backend audit log when endpoint is ready
577+
// Store locally for now
578+
const existingEvents = JSON.parse(
579+
localStorage.getItem("auditEvents") || "[]"
580+
);
581+
existingEvents.push(auditEvent);
582+
localStorage.setItem("auditEvents", JSON.stringify(existingEvents));
583+
}
584+
471585
async clear() {
472586
await this.#store.delete("vault");
473587
}

infrastructure/eid-wallet/src/lib/global/controllers/user.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import type { Store } from "@tauri-apps/plugin-store";
22

3+
/**
4+
* User assurance levels - determines what identity bindings exist
5+
*/
6+
export enum AssuranceLevel {
7+
/** Baseline identity only - no KYC completed */
8+
UNVERIFIED = "UNVERIFIED",
9+
/** KYC completed - Physical ID binding exists */
10+
KYC_VERIFIED = "KYC_VERIFIED",
11+
}
12+
313
/**
414
* @author SoSweetHam <soham@auvo.io>
515
* @version 0.0.1-alpha/Stub
@@ -10,6 +20,7 @@ import type { Store } from "@tauri-apps/plugin-store";
1020
*
1121
* Uses the following namespaces in the store:
1222
* - `user` - The user state
23+
* - `assuranceLevel` - The user's current assurance level (UNVERIFIED | KYC_VERIFIED)
1324
*
1425
* @memberof GlobalState
1526
* You should not use this class directly, it is intended for use through the GlobalState.
@@ -144,9 +155,40 @@ export class UserController {
144155
});
145156
}
146157

158+
/**
159+
* Sets the user's assurance level (UNVERIFIED | KYC_VERIFIED)
160+
*/
161+
set assuranceLevel(level: Promise<AssuranceLevel | undefined> | AssuranceLevel | undefined) {
162+
if (level instanceof Promise) {
163+
level.then((resolved) => {
164+
this.#store.set("assuranceLevel", resolved);
165+
}).catch((error) => {
166+
console.error("Failed to set assurance level:", error);
167+
});
168+
} else {
169+
this.#store.set("assuranceLevel", level);
170+
}
171+
}
172+
173+
/**
174+
* Gets the user's assurance level, defaults to UNVERIFIED
175+
*/
176+
get assuranceLevel() {
177+
return this.#store
178+
.get<AssuranceLevel>("assuranceLevel")
179+
.then((level) => {
180+
return level ?? AssuranceLevel.UNVERIFIED;
181+
})
182+
.catch((error) => {
183+
console.error("Failed to get assurance level:", error);
184+
return AssuranceLevel.UNVERIFIED;
185+
});
186+
}
187+
147188
async clear() {
148189
await this.#store.delete("user");
149190
await this.#store.delete("fake");
150191
await this.#store.delete("doc");
192+
await this.#store.delete("assuranceLevel");
151193
}
152194
}

infrastructure/eid-wallet/src/routes/(app)/ePassport/+page.svelte

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<script lang="ts">
2+
import { goto } from "$app/navigation";
23
import { AppNav, IdentityCard } from "$lib/fragments";
34
import type { GlobalState } from "$lib/global";
5+
import { AssuranceLevel } from "$lib/global/controllers/user";
6+
import { ButtonAction } from "$lib/ui";
47
import { getContext, onMount } from "svelte";
58
69
const globalState = getContext<() => GlobalState>("globalState")();
@@ -9,14 +12,31 @@ function shareEPassport() {
912
alert("EPassport Code shared!");
1013
}
1114
12-
let userData: Record<string, string | boolean | undefined>;
13-
let docData: Record<string, unknown> = {};
15+
let userData: Record<string, string | boolean | undefined> = $state({});
16+
let docData: Record<string, unknown> = $state({});
17+
let assuranceLevel = $state<AssuranceLevel>(AssuranceLevel.UNVERIFIED);
18+
19+
/**
20+
* Navigate to KYC verification
21+
* This allows UNVERIFIED users to upgrade their identity from ePassport page
22+
*/
23+
async function handleVerifyIdentity() {
24+
console.log("Navigating to KYC verification from ePassport page");
25+
globalState.vaultController.emitAuditEvent("KYC_UPGRADE_INITIATED", {
26+
source: "epassport_page",
27+
timestamp: new Date().toISOString(),
28+
});
29+
await goto("/verify");
30+
}
1431
1532
onMount(async () => {
1633
const userInfo = await globalState.userController.user;
1734
const isFake = await globalState.userController.isFake;
1835
docData = (await globalState.userController.document) ?? {};
1936
userData = { ...userInfo, isFake };
37+
assuranceLevel =
38+
(await globalState.userController.assuranceLevel) ??
39+
AssuranceLevel.UNVERIFIED;
2040
console.log("loggg", userData);
2141
});
2242
</script>
@@ -27,7 +47,46 @@ onMount(async () => {
2747
{#if userData}
2848
<IdentityCard variant="ePassport" {userData} class="shadow-lg" />
2949
{/if}
30-
{#if docData}
50+
51+
<!-- Verify Identity CTA for UNVERIFIED users -->
52+
{#if assuranceLevel === AssuranceLevel.UNVERIFIED}
53+
<div
54+
class="mt-4 p-4 bg-linear-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 border border-blue-200 dark:border-blue-800 rounded-xl"
55+
>
56+
<div class="flex items-center gap-3 mb-3">
57+
<div
58+
class="w-10 h-10 rounded-full bg-linear-to-br from-blue-500 to-purple-600 flex items-center justify-center"
59+
>
60+
<svg
61+
class="w-5 h-5 text-white"
62+
fill="none"
63+
stroke="currentColor"
64+
viewBox="0 0 24 24"
65+
>
66+
<path
67+
stroke-linecap="round"
68+
stroke-linejoin="round"
69+
stroke-width="2"
70+
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
71+
/>
72+
</svg>
73+
</div>
74+
<div>
75+
<p class="font-semibold text-gray-900 dark:text-white">
76+
Verify Your Identity
77+
</p>
78+
<p class="text-sm text-gray-600 dark:text-gray-400">
79+
Complete KYC to unlock all features
80+
</p>
81+
</div>
82+
</div>
83+
<ButtonAction callback={handleVerifyIdentity} class="w-full">
84+
Start Verification
85+
</ButtonAction>
86+
</div>
87+
{/if}
88+
89+
{#if docData && Object.keys(docData).length > 0}
3190
<div
3291
class="p-6 pt-12 bg-gray w-full rounded-2xl -mt-8 flex flex-col gap-2"
3392
>

0 commit comments

Comments
 (0)