An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: address formatting violations in Rust code

- Fixed function signature line length in get_stored_did_doc (apps/identity-wallet/src-tauri/src/lib.rs:683)
- Fixed .map_err chain formatting (apps/identity-wallet/src-tauri/src/lib.rs:687-690)
- Applied cargo fmt --all to automatically fix all formatting issues

Verified with cargo fmt --all --check (no violations remain)

authored by

Malpercio and committed by
Tangled
4be11f35 d1be546a

+1662 -32
+7 -4
apps/identity-wallet/src-tauri/src/lib.rs
··· 680 680 /// The frontend uses this to extract identity information (handle, PDS URL) for 681 681 /// multi-identity card display in IdentityListHome. 682 682 #[tauri::command] 683 - fn get_stored_did_doc(did: String) -> Result<Option<serde_json::Value>, identity_store::IdentityStoreError> { 683 + fn get_stored_did_doc( 684 + did: String, 685 + ) -> Result<Option<serde_json::Value>, identity_store::IdentityStoreError> { 684 686 let store = identity_store::IdentityStore; 685 687 match store.get_did_doc(&did)? { 686 688 Some(json_str) => { 687 - let value: serde_json::Value = serde_json::from_str(&json_str) 688 - .map_err(|e| identity_store::IdentityStoreError::SerializationError { 689 + let value: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| { 690 + identity_store::IdentityStoreError::SerializationError { 689 691 message: e.to_string(), 690 - })?; 692 + } 693 + })?; 691 694 Ok(Some(value)) 692 695 } 693 696 None => Ok(None),
+34 -11
crates/relay/src/routes/delete_handle.rs
··· 50 50 ApiError::new(ErrorCode::InternalError, "failed to look up handle") 51 51 })?; 52 52 53 - let (owner_did,) = row.ok_or_else(|| ApiError::new(ErrorCode::HandleNotFound, "handle not found"))?; 53 + let (owner_did,) = 54 + row.ok_or_else(|| ApiError::new(ErrorCode::HandleNotFound, "handle not found"))?; 54 55 55 56 // Step 3: Verify ownership — session DID must match the handle owner. 56 57 if session.did != owner_did { ··· 155 156 async fn happy_path_deletes_handle_no_dns_provider() { 156 157 let state = test_state().await; 157 158 let db = state.db.clone(); 158 - let did = format!("did:plc:{}", &Uuid::new_v4().to_string().replace('-', "")[..24]); 159 + let did = format!( 160 + "did:plc:{}", 161 + &Uuid::new_v4().to_string().replace('-', "")[..24] 162 + ); 159 163 let handle = format!("alice.{}", state.config.available_user_domains[0]); 160 164 161 165 seed_handle(&db, &handle, &did).await; ··· 183 187 async fn dns_provider_success_deletes_handle_and_dns() { 184 188 let state = state_with_ok_dns().await; 185 189 let db = state.db.clone(); 186 - let did = format!("did:plc:{}", &Uuid::new_v4().to_string().replace('-', "")[..24]); 190 + let did = format!( 191 + "did:plc:{}", 192 + &Uuid::new_v4().to_string().replace('-', "")[..24] 193 + ); 187 194 let handle = format!("alice.{}", state.config.available_user_domains[0]); 188 195 189 196 seed_handle(&db, &handle, &did).await; ··· 202 209 .fetch_optional(&db) 203 210 .await 204 211 .unwrap(); 205 - assert!(row.is_none(), "handle row must be removed when DNS succeeds"); 212 + assert!( 213 + row.is_none(), 214 + "handle row must be removed when DNS succeeds" 215 + ); 206 216 } 207 217 208 218 /// DNS provider fails: returns 502 DNS_ERROR and the DB row is NOT deleted. ··· 210 220 async fn dns_provider_failure_returns_502_and_row_survives() { 211 221 let state = state_with_err_dns().await; 212 222 let db = state.db.clone(); 213 - let did = format!("did:plc:{}", &Uuid::new_v4().to_string().replace('-', "")[..24]); 223 + let did = format!( 224 + "did:plc:{}", 225 + &Uuid::new_v4().to_string().replace('-', "")[..24] 226 + ); 214 227 let handle = format!("alice.{}", state.config.available_user_domains[0]); 215 228 216 229 seed_handle(&db, &handle, &did).await; ··· 250 263 async fn missing_auth_returns_401() { 251 264 let state = test_state().await; 252 265 let db = state.db.clone(); 253 - let did = format!("did:plc:{}", &Uuid::new_v4().to_string().replace('-', "")[..24]); 266 + let did = format!( 267 + "did:plc:{}", 268 + &Uuid::new_v4().to_string().replace('-', "")[..24] 269 + ); 254 270 let handle = format!("alice.{}", state.config.available_user_domains[0]); 255 271 seed_handle(&db, &handle, &did).await; 256 272 ··· 274 290 let db = state.db.clone(); 275 291 276 292 // Owner account + handle. 277 - let owner_did = 278 - format!("did:plc:{}", &Uuid::new_v4().to_string().replace('-', "")[..24]); 293 + let owner_did = format!( 294 + "did:plc:{}", 295 + &Uuid::new_v4().to_string().replace('-', "")[..24] 296 + ); 279 297 let handle = format!("alice.{}", state.config.available_user_domains[0]); 280 298 seed_handle(&db, &handle, &owner_did).await; 281 299 282 300 // Different account that tries to delete the handle. 283 - let other_did = 284 - format!("did:plc:{}", &Uuid::new_v4().to_string().replace('-', "")[..24]); 301 + let other_did = format!( 302 + "did:plc:{}", 303 + &Uuid::new_v4().to_string().replace('-', "")[..24] 304 + ); 285 305 sqlx::query( 286 306 "INSERT INTO accounts (did, email, password_hash, created_at, updated_at) \ 287 307 VALUES (?, ?, NULL, datetime('now'), datetime('now'))", ··· 316 336 async fn nonexistent_handle_returns_404() { 317 337 let state = test_state().await; 318 338 let db = state.db.clone(); 319 - let did = format!("did:plc:{}", &Uuid::new_v4().to_string().replace('-', "")[..24]); 339 + let did = format!( 340 + "did:plc:{}", 341 + &Uuid::new_v4().to_string().replace('-', "")[..24] 342 + ); 320 343 321 344 sqlx::query( 322 345 "INSERT INTO accounts (did, email, password_hash, created_at, updated_at) \
+8 -6
crates/relay/src/routes/request_password_reset.rs
··· 161 161 .await 162 162 .unwrap(); 163 163 164 - let count: i64 = 165 - sqlx::query_scalar("SELECT COUNT(*) FROM password_reset_tokens") 166 - .fetch_one(&db) 167 - .await 168 - .unwrap(); 164 + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM password_reset_tokens") 165 + .fetch_one(&db) 166 + .await 167 + .unwrap(); 169 168 assert_eq!(count, 0, "no token should be inserted for unknown email"); 170 169 } 171 170 ··· 208 207 .fetch_one(&db) 209 208 .await 210 209 .unwrap(); 211 - assert!(diff < 5, "expiry should be ~1 hour from now, got {diff}s drift"); 210 + assert!( 211 + diff < 5, 212 + "expiry should be ~1 hour from now, got {diff}s drift" 213 + ); 212 214 } 213 215 }
+20 -11
crates/relay/src/routes/reset_password.rs
··· 45 45 })?; 46 46 47 47 // --- Look up token (expiry evaluated in the same query) --- 48 - let row = get_reset_token(&mut tx, &token_hash).await?.ok_or_else(|| { 49 - ApiError::new(ErrorCode::InvalidToken, "invalid or unknown reset token") 50 - })?; 48 + let row = get_reset_token(&mut tx, &token_hash) 49 + .await? 50 + .ok_or_else(|| ApiError::new(ErrorCode::InvalidToken, "invalid or unknown reset token"))?; 51 51 52 52 // --- Validate: not already used --- 53 53 // Check used_at first — a consumed token is non-recoverable regardless of expiry. ··· 233 233 .fetch_one(&db) 234 234 .await 235 235 .unwrap(); 236 - assert!(used_at.is_some(), "used_at must be set after successful reset"); 236 + assert!( 237 + used_at.is_some(), 238 + "used_at must be set after successful reset" 239 + ); 237 240 } 238 241 239 242 #[tokio::test] ··· 442 445 assert_eq!(response.status(), StatusCode::OK); 443 446 444 447 // No token inserted. 445 - let count: i64 = 446 - sqlx::query_scalar("SELECT COUNT(*) FROM password_reset_tokens") 447 - .fetch_one(&db) 448 - .await 449 - .unwrap(); 450 - assert_eq!(count, 0, "no token should be inserted for deactivated account"); 448 + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM password_reset_tokens") 449 + .fetch_one(&db) 450 + .await 451 + .unwrap(); 452 + assert_eq!( 453 + count, 0, 454 + "no token should be inserted for deactivated account" 455 + ); 451 456 } 452 457 453 458 /// Multiple outstanding tokens per DID are allowed — each is valid independently. ··· 478 483 .oneshot(post_reset_password(&token2, "newpass2")) 479 484 .await 480 485 .unwrap(); 481 - assert_eq!(r2.status(), StatusCode::OK, "second token must remain valid after first is used"); 486 + assert_eq!( 487 + r2.status(), 488 + StatusCode::OK, 489 + "second token must remain valid after first is used" 490 + ); 482 491 } 483 492 }
+276
docs/implementation-plans/2026-03-28-plc-key-management/phase_01.md
··· 1 + # Claim Flow Frontend — Phase 1: Foundation 2 + 3 + **Goal:** Add `list_identities` Tauri command, create ModeSelectScreen component, wire mode selector as new entry point with identity-aware onMount logic. 4 + 5 + **Architecture:** Extends the existing screen state machine in `+page.svelte` with a `mode_select` step that replaces `relay_config` as the first-launch entry point. Adds a thin Tauri command wrapper for `IdentityStore::list_identities()` so the frontend can check for existing identities on mount. 6 + 7 + **Tech Stack:** Svelte 5 (runes), TypeScript, Tauri v2 IPC, Rust 8 + 9 + **Scope:** 1 of 5 implementation phases (Phase 5 of the plc-key-management design) 10 + 11 + **Codebase verified:** 2026-03-29 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### plc-key-management.AC5: Import flow frontend 20 + - **plc-key-management.AC5.1 Success:** Mode selector on first launch shows "Create new identity" and "I have an identity" options 21 + - **plc-key-management.AC5.2 Success:** App skips mode selector and goes to home when `listIdentities()` returns non-empty 22 + 23 + --- 24 + 25 + <!-- START_SUBCOMPONENT_A (tasks 1-2) --> 26 + <!-- START_TASK_1 --> 27 + ### Task 1: Add `list_identities` Tauri command and IPC wrapper 28 + 29 + **Verifies:** plc-key-management.AC5.2 (provides the command the frontend needs to check for existing identities) 30 + 31 + **Files:** 32 + - Modify: `apps/identity-wallet/src-tauri/src/lib.rs:670-762` (add command + register in invoke_handler) 33 + - Modify: `apps/identity-wallet/src/lib/ipc.ts:420-471` (add wrapper + error type) 34 + 35 + **Implementation:** 36 + 37 + **Rust side** — add a `#[tauri::command]` in `lib.rs` that wraps `IdentityStore::list_identities()`. The `IdentityStore` is a unit struct (no Tauri `State<>` needed). Place the command immediately before the `check_handle_resolution` command (around line 663). Add `IdentityStoreError` to the imports and register `list_identities` in `tauri::generate_handler![]`. 38 + 39 + The command signature: 40 + ```rust 41 + #[tauri::command] 42 + fn list_identities() -> Result<Vec<String>, identity_store::IdentityStoreError> { 43 + identity_store::IdentityStore.list_identities() 44 + } 45 + ``` 46 + 47 + `IdentityStoreError` already derives `Serialize` with `#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "code")]`, so it works as a Tauri IPC error type out of the box. 48 + 49 + **TypeScript side** — add to `ipc.ts` after the claim flow section (after line 470): 50 + 51 + ```typescript 52 + // ── Identity Store ────────────────────────────────────────────────────── 53 + 54 + export type IdentityStoreError = 55 + | { code: 'IDENTITY_NOT_FOUND' } 56 + | { code: 'IDENTITY_ALREADY_EXISTS' } 57 + | { code: 'KEYCHAIN_ERROR' } 58 + | { code: 'KEY_GENERATION_FAILED' } 59 + | { code: 'SERIALIZATION_ERROR' }; 60 + 61 + export const listIdentities = (): Promise<string[]> => 62 + invoke('list_identities'); 63 + ``` 64 + 65 + **Testing:** 66 + 67 + The Rust `IdentityStore::list_identities()` is already tested in `identity_store.rs` (lines 517-547). The Tauri command is a thin passthrough — no additional test needed. 68 + 69 + **Verification:** 70 + Run: `cd apps/identity-wallet && pnpm check` 71 + Expected: No type errors 72 + 73 + Run: `cargo build -p identity-wallet --lib` 74 + Expected: Compiles without errors 75 + 76 + **Commit:** `feat(identity-wallet): add list_identities Tauri command and IPC wrapper` 77 + <!-- END_TASK_1 --> 78 + 79 + <!-- START_TASK_2 --> 80 + ### Task 2: Create ModeSelectScreen component 81 + 82 + **Verifies:** plc-key-management.AC5.1 83 + 84 + **Files:** 85 + - Create: `apps/identity-wallet/src/lib/components/onboarding/ModeSelectScreen.svelte` 86 + 87 + **Implementation:** 88 + 89 + Create a new screen component following the existing pattern (see `WelcomeScreen.svelte` for reference). The component receives two callbacks via `$props()`: 90 + 91 + ```svelte 92 + <script lang="ts"> 93 + let { oncreate, onimport }: { oncreate: () => void; onimport: () => void } = $props(); 94 + </script> 95 + 96 + <div class="screen"> 97 + <div class="brand"> 98 + <h1>Identity Wallet</h1> 99 + <p class="tagline">Your self-sovereign identity, in your pocket.</p> 100 + </div> 101 + <div class="actions"> 102 + <button class="cta" onclick={oncreate}>Create new identity</button> 103 + <button class="cta cta--secondary" onclick={onimport}>I have an identity</button> 104 + </div> 105 + </div> 106 + ``` 107 + 108 + Style the component consistently with WelcomeScreen: centered layout, `.cta` button style for primary action, `.cta--secondary` for the import option. Use the same spacing, colors, and border-radius as existing screens (`#007aff` primary, `#f3f4f6` secondary background, `12px` border-radius, `1.1rem` font-size). 109 + 110 + **Testing:** 111 + 112 + No automated frontend tests — this is a Svelte component in a WKWebView app. Verification is operational + manual. 113 + 114 + **Verification:** 115 + Run: `cd apps/identity-wallet && pnpm check` 116 + Expected: No type errors 117 + 118 + **Commit:** `feat(identity-wallet): add ModeSelectScreen component` 119 + <!-- END_TASK_2 --> 120 + <!-- END_SUBCOMPONENT_A --> 121 + 122 + <!-- START_SUBCOMPONENT_B (tasks 3-4) --> 123 + <!-- START_TASK_3 --> 124 + ### Task 3: Wire mode selector into +page.svelte state machine 125 + 126 + **Verifies:** plc-key-management.AC5.1, plc-key-management.AC5.2 127 + 128 + **Files:** 129 + - Modify: `apps/identity-wallet/src/routes/+page.svelte:1-282` 130 + 131 + **Implementation:** 132 + 133 + **Step 1: Add imports** (top of `<script>` section) 134 + - Import `ModeSelectScreen` from `$lib/components/onboarding/ModeSelectScreen.svelte` 135 + - Import `listIdentities` from `$lib/ipc` 136 + 137 + **Step 2: Expand `OnboardingStep` type** (lines 31-48) 138 + 139 + Add new steps for the import flow. The full type becomes: 140 + ```typescript 141 + type OnboardingStep = 142 + | 'mode_select' 143 + | 'relay_config' 144 + | 'welcome' 145 + | 'claim_code' 146 + | 'email' 147 + | 'handle' 148 + | 'password' 149 + | 'loading' 150 + | 'did_ceremony' 151 + | 'did_success' 152 + | 'shamir_backup' 153 + | 'handle_registration' 154 + | 'complete' 155 + | 'authenticating' 156 + | 'home' 157 + | 'did_document' 158 + | 'recovery_info' 159 + | 'auth_failed' 160 + | 'identity_input' 161 + | 'pds_auth' 162 + | 'email_verification' 163 + | 'review_operation' 164 + | 'claim_success'; 165 + ``` 166 + 167 + **Step 3: Change initial step** (line 52) 168 + 169 + Change from `'relay_config'` to `'mode_select'`: 170 + ```typescript 171 + let step = $state<OnboardingStep>('mode_select'); 172 + ``` 173 + 174 + **Step 4: Update `onMount`** (lines 76-92) 175 + 176 + Replace the current onMount logic. The new flow: 177 + 1. Check `listIdentities()` — if non-empty, skip directly to `home` 178 + 2. Otherwise stay at `mode_select` (the default initial step) 179 + 3. Keep the `auth_ready` listener for the existing onboarding OAuth flow 180 + 181 + ```typescript 182 + onMount(async () => { 183 + // If the user has claimed identities, skip to home. 184 + try { 185 + const identities = await listIdentities(); 186 + if (identities.length > 0) { 187 + step = 'home'; 188 + return; 189 + } 190 + } catch { 191 + // listIdentities failed (e.g. empty Keychain on first launch) — continue to mode_select 192 + } 193 + 194 + // Legacy user fallback: if a relay URL is already configured (from the old 195 + // single-identity flow before multi-identity), the user has used the app before 196 + // but has no managed-dids entry. Skip relay_config but still show mode_select 197 + // so they can choose create vs. import. Without this, legacy users would see 198 + // mode_select and then relay_config (asking them to configure a relay they 199 + // already configured). 200 + // Note: mode_select is already the default step, so this is a no-op for 201 + // mode_select itself, but it prevents the "Create new identity" path from 202 + // redundantly showing relay_config when the relay is already configured. 203 + // The relay_config screen itself already checks getRelayUrl() internally. 204 + 205 + // Listen for auth_ready from relay OAuth (existing onboarding flow). 206 + listen('auth_ready', () => { 207 + goTo('home'); 208 + }); 209 + }); 210 + ``` 211 + 212 + **Note:** PDS auth completion is handled by PdsAuthScreen via promise resolution callback (Phase 3), NOT via a separate event listener. This matches the AuthenticatingScreen pattern. 213 + 214 + **Note on legacy users:** Users who configured a relay URL via the old single-identity flow (before multi-identity) will have `listIdentities()` return empty (no `managed-dids` Keychain entry) but will have a saved relay URL. These users see `mode_select` as their entry point, which is correct — they need to either create a new identity (goes to relay_config, which will detect the saved URL and skip the input) or import an existing one. The existing `RelayConfigScreen` already checks for a saved URL and pre-fills it, so the "Create new identity" path works correctly for legacy users without any additional migration. Full identity migration (moving flat Keychain data to per-DID format) is out of scope for Phase 5 and would be addressed in a future phase if needed. 215 + 216 + **Step 5: Add mode_select rendering** (in the `{#if}` chain, at the top before `relay_config`) 217 + 218 + Insert as the first condition in the `{#if}` chain: 219 + ```svelte 220 + {#if step === 'mode_select'} 221 + <ModeSelectScreen 222 + oncreate={() => goTo('relay_config')} 223 + onimport={() => goTo('identity_input')} 224 + /> 225 + {:else if step === 'relay_config'} 226 + ``` 227 + 228 + The "Create new identity" path goes to `relay_config` → existing onboarding flow (unchanged). 229 + The "I have an identity" path goes to `identity_input` (will be wired to IdentityInputScreen in Phase 2). 230 + 231 + **Note:** The `identity_input` step has no rendering block yet — it will be added in Phase 2. If somehow navigated to before Phase 2, the app shows a blank screen (no crash). This is acceptable for incremental development. 232 + 233 + **Testing:** 234 + 235 + No automated frontend tests for UI components. AC5.1 and AC5.2 require human verification on the iOS Simulator. 236 + 237 + **Verification:** 238 + Run: `cd apps/identity-wallet && pnpm check` 239 + Expected: No type errors (svelte-check validates all Svelte files and TypeScript) 240 + 241 + **Commit:** `feat(identity-wallet): wire mode selector as entry point with identity-aware onMount` 242 + <!-- END_TASK_3 --> 243 + 244 + <!-- START_TASK_4 --> 245 + ### Task 4: Verify existing onboarding flow unchanged 246 + 247 + **Verifies:** plc-key-management.AC5.13 (partial — verifies the wiring doesn't break existing flow) 248 + 249 + **Files:** 250 + - No file changes — verification only 251 + 252 + **Implementation:** 253 + 254 + This is a verification-only task. After the mode selector changes: 255 + 256 + 1. The "Create new identity" path from mode_select → relay_config → welcome → claim_code → email → handle → password → loading → did_ceremony → did_success → shamir_backup → handle_registration → complete → authenticating → home should remain intact. 257 + 258 + 2. All existing screen components receive the same props as before. 259 + 260 + 3. The `submitAccount` function and `handleError` function are unchanged. 261 + 262 + 4. The `auth_ready` listener still routes to `home` for the existing OAuth flow. 263 + 264 + **Verification:** 265 + Run: `cd apps/identity-wallet && pnpm check` 266 + Expected: No type errors 267 + 268 + Run: `cargo build -p identity-wallet --lib` 269 + Expected: Compiles without errors 270 + 271 + Run: `cargo test -p identity-wallet` 272 + Expected: All existing Rust tests pass 273 + 274 + **Commit:** No commit — verification only 275 + <!-- END_TASK_4 --> 276 + <!-- END_SUBCOMPONENT_B -->
+148
docs/implementation-plans/2026-03-28-plc-key-management/phase_02.md
··· 1 + # Claim Flow Frontend — Phase 2: IdentityInputScreen 2 + 3 + **Goal:** Create IdentityInputScreen component that accepts a handle or DID, resolves it via `resolveIdentity()`, displays identity info (PDS, rotation keys), and hands off resolved data to the parent page. 4 + 5 + **Architecture:** Self-contained screen component that manages its own async resolution state. The parent stores the `IdentityInfo` result and uses it across subsequent import flow screens. Adds import flow state variables (`identityInfo`, `verifiedClaim`, `claimResult`) to `+page.svelte` for cross-screen data sharing. 6 + 7 + **Tech Stack:** Svelte 5 (runes), TypeScript, Tauri v2 IPC 8 + 9 + **Scope:** 2 of 5 implementation phases 10 + 11 + **Codebase verified:** 2026-03-29 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### plc-key-management.AC5: Import flow frontend 20 + - **plc-key-management.AC5.3 Success:** IdentityInputScreen accepts handle or DID, calls `resolveIdentity`, and displays resolved identity info (DID, handle, PDS URL, rotation key status) 21 + - **plc-key-management.AC5.4 Failure:** IdentityInputScreen shows user-friendly error when resolution fails (handle not found, DID not found, PDS unreachable, network error) 22 + 23 + --- 24 + 25 + <!-- START_SUBCOMPONENT_A (tasks 1-2) --> 26 + <!-- START_TASK_1 --> 27 + ### Task 1: Create IdentityInputScreen component 28 + 29 + **Verifies:** plc-key-management.AC5.3, plc-key-management.AC5.4 30 + 31 + **Files:** 32 + - Create: `apps/identity-wallet/src/lib/components/onboarding/IdentityInputScreen.svelte` 33 + 34 + **Implementation:** 35 + 36 + Create a new screen component that handles identity resolution internally. Unlike the simple input screens (HandleScreen, EmailScreen) that delegate async work to the parent, this screen manages its own async state because it needs to display resolution results before the user can proceed. 37 + 38 + **Props interface:** 39 + ```typescript 40 + let { 41 + value = $bindable(''), 42 + onnext, 43 + onback, 44 + }: { 45 + value: string; 46 + onnext: (info: IdentityInfo) => void; 47 + onback: () => void; 48 + } = $props(); 49 + ``` 50 + 51 + **Internal state:** 52 + ```typescript 53 + let resolving = $state(false); 54 + let resolved = $state<IdentityInfo | null>(null); 55 + let error = $state<string | null>(null); 56 + ``` 57 + 58 + **Behavior:** 59 + 1. Text input for handle or DID (`bind:value`) 60 + 2. "Resolve" button (disabled while `resolving` or when input is empty) 61 + 3. On resolve: call `resolveIdentity(value.trim())` from `$lib/ipc` 62 + 4. On success: set `resolved` to the returned `IdentityInfo`, clear `error` 63 + 5. On error: map `ResolveError.code` to user-friendly messages: 64 + - `HANDLE_NOT_FOUND` → "Handle not found. Check the spelling and try again." 65 + - `DID_NOT_FOUND` → "DID not found on PLC directory." 66 + - `PDS_UNREACHABLE` → "Could not reach the PDS. It may be temporarily offline." 67 + - `NETWORK_ERROR` → "Network error. Check your connection and try again." 68 + 6. When `resolved` is non-null, display identity info card: 69 + - Handle: `@{resolved.handle}` 70 + - DID: truncated (reuse the same truncation pattern from HomeScreen) 71 + - PDS: `resolved.pdsUrl` 72 + - Rotation key status: "Your device is the root key" (green) if `deviceKeyIsRoot`, or "Device key is not the root key" (neutral) if not 73 + 7. "Continue" button appears only when resolved — calls `onnext(resolved)` 74 + 8. "Back" button at top or bottom — calls `onback()` 75 + 9. If user changes the input after resolving, clear `resolved` and `error` 76 + 77 + **Styling:** Follow existing screen patterns — `.screen` container, centered layout, `#007aff` primary buttons, `12px` border-radius, `.error-text` for errors. The identity info card should use the same `.identity-card` pattern from HomeScreen (background: `#f9fafb`, border: `1px solid #d1d5db`, rounded corners). 78 + 79 + **Verification:** 80 + Run: `cd apps/identity-wallet && pnpm check` 81 + Expected: No type errors 82 + 83 + **Commit:** `feat(identity-wallet): add IdentityInputScreen component` 84 + <!-- END_TASK_1 --> 85 + 86 + <!-- START_TASK_2 --> 87 + ### Task 2: Wire IdentityInputScreen into +page.svelte 88 + 89 + **Verifies:** plc-key-management.AC5.3, plc-key-management.AC5.4 90 + 91 + **Files:** 92 + - Modify: `apps/identity-wallet/src/routes/+page.svelte` 93 + 94 + **Implementation:** 95 + 96 + **Step 1: Add import flow state variables** (after the existing `homeData` state, around line 65) 97 + 98 + ```typescript 99 + // ── Import flow state ──────────────────────────────────────────────────── 100 + let identityInfo = $state<IdentityInfo | null>(null); 101 + let verifiedClaim = $state<VerifiedClaimOp | null>(null); 102 + let claimResult = $state<ClaimResult | null>(null); 103 + ``` 104 + 105 + Add the necessary type imports from `$lib/ipc`: 106 + ```typescript 107 + import { ..., type IdentityInfo, type VerifiedClaimOp, type ClaimResult } from '$lib/ipc'; 108 + ``` 109 + 110 + **Step 2: Extend the `form` object** (line 53) 111 + 112 + Add `handleOrDid` to the form: 113 + ```typescript 114 + let form = $state({ claimCode: '', email: '', handle: '', password: '', did: '', share3: '', registeredHandle: '', handleOrDid: '' }); 115 + ``` 116 + 117 + **Step 3: Import IdentityInputScreen** (in the imports section) 118 + 119 + ```typescript 120 + import IdentityInputScreen from '$lib/components/onboarding/IdentityInputScreen.svelte'; 121 + ``` 122 + 123 + **Step 4: Add rendering block** (in the `{#if}` chain, after the mode_select block added in Phase 1) 124 + 125 + Insert after the `mode_select` block, before any existing blocks that don't need to change: 126 + 127 + ```svelte 128 + {:else if step === 'identity_input'} 129 + <IdentityInputScreen 130 + bind:value={form.handleOrDid} 131 + onnext={(info) => { 132 + identityInfo = info; 133 + goTo('pds_auth'); 134 + }} 135 + onback={() => goTo('mode_select')} 136 + /> 137 + ``` 138 + 139 + **Verification:** 140 + Run: `cd apps/identity-wallet && pnpm check` 141 + Expected: No type errors 142 + 143 + Run: `cargo build -p identity-wallet --lib` 144 + Expected: Compiles without errors 145 + 146 + **Commit:** `feat(identity-wallet): wire IdentityInputScreen into page state machine` 147 + <!-- END_TASK_2 --> 148 + <!-- END_SUBCOMPONENT_A -->
+239
docs/implementation-plans/2026-03-28-plc-key-management/phase_03.md
··· 1 + # Claim Flow Frontend — Phase 3: PdsAuthScreen + EmailVerificationScreen 2 + 3 + **Goal:** Create screens for PDS authentication (OAuth to the user's old PDS) and email verification (token input + claim signing). These are the middle steps of the import flow. 4 + 5 + **Architecture:** PdsAuthScreen follows the AuthenticatingScreen pattern — calls `startPdsAuth()` on user action, shows spinner while Safari handles OAuth, navigates on promise resolution. EmailVerificationScreen sends the verification email on mount, accepts a token input, and calls `signAndVerifyClaim()` to produce a `VerifiedClaimOp`. 6 + 7 + **Tech Stack:** Svelte 5 (runes), TypeScript, Tauri v2 IPC 8 + 9 + **Scope:** 3 of 5 implementation phases 10 + 11 + **Codebase verified:** 2026-03-29 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### plc-key-management.AC5: Import flow frontend 20 + - **plc-key-management.AC5.5 Success:** PdsAuthScreen shows PDS endpoint, user taps Authenticate, Safari opens, and on successful deep-link callback the flow advances to email verification 21 + - **plc-key-management.AC5.6 Failure:** PdsAuthScreen shows error message when PDS auth fails (UNAUTHORIZED, NETWORK_ERROR) 22 + - **plc-key-management.AC5.7 Success:** EmailVerificationScreen sends verification email on mount, accepts token input, and produces a VerifiedClaimOp on successful signAndVerifyClaim 23 + - **plc-key-management.AC5.8 Failure:** EmailVerificationScreen shows error when token is invalid (INVALID_TOKEN) or verification fails (VERIFICATION_FAILED) 24 + 25 + --- 26 + 27 + <!-- START_SUBCOMPONENT_A (tasks 1-2) --> 28 + <!-- START_TASK_1 --> 29 + ### Task 1: Create PdsAuthScreen component 30 + 31 + **Verifies:** plc-key-management.AC5.5, plc-key-management.AC5.6 32 + 33 + **Files:** 34 + - Create: `apps/identity-wallet/src/lib/components/onboarding/PdsAuthScreen.svelte` 35 + 36 + **Implementation:** 37 + 38 + Create a screen component following the AuthenticatingScreen pattern but with a user-initiated action instead of auto-start on mount. 39 + 40 + **Props interface:** 41 + ```typescript 42 + let { 43 + pdsUrl, 44 + onnext, 45 + onback, 46 + }: { 47 + pdsUrl: string; 48 + onnext: () => void; 49 + onback: () => void; 50 + } = $props(); 51 + ``` 52 + 53 + **Internal state:** 54 + ```typescript 55 + let authenticating = $state(false); 56 + let error = $state<string | null>(null); 57 + ``` 58 + 59 + **Behavior:** 60 + 1. **Initial state:** Display PDS info and an "Authenticate with PDS" button 61 + - Show: "Connect to your PDS at `{pdsUrl}` to verify your identity." 62 + - Show back button to return to identity input 63 + 2. **On button press:** Set `authenticating = true`, call `startPdsAuth(pdsUrl)` from `$lib/ipc` 64 + - While authenticating: show spinner + "Opening browser for PDS authentication…" (matching AuthenticatingScreen style) 65 + - Disable back button while authenticating 66 + 3. **On success** (promise resolves): call `onnext()` — parent navigates to `email_verification` 67 + 4. **On error:** Set `authenticating = false`, map `ClaimError.code` to user-friendly messages: 68 + - `UNAUTHORIZED` → "Authentication was denied. Please try again." 69 + - `NETWORK_ERROR` → "Network error. Check your connection and try again." 70 + - Any other code → "Authentication failed. Please try again." 71 + - Show error text + retry button (re-click "Authenticate with PDS") 72 + 73 + **Styling:** Follow existing screen patterns. Use the spinner from AuthenticatingScreen (`.spinner` with border-top animation). Use `.error-text` pattern for errors. 74 + 75 + **Verification:** 76 + Run: `cd apps/identity-wallet && pnpm check` 77 + Expected: No type errors 78 + 79 + **Commit:** `feat(identity-wallet): add PdsAuthScreen component` 80 + <!-- END_TASK_1 --> 81 + 82 + <!-- START_TASK_2 --> 83 + ### Task 2: Wire PdsAuthScreen into +page.svelte 84 + 85 + **Verifies:** plc-key-management.AC5.5, plc-key-management.AC5.6 86 + 87 + **Files:** 88 + - Modify: `apps/identity-wallet/src/routes/+page.svelte` 89 + 90 + **Implementation:** 91 + 92 + **Step 1: Import PdsAuthScreen** 93 + ```typescript 94 + import PdsAuthScreen from '$lib/components/onboarding/PdsAuthScreen.svelte'; 95 + ``` 96 + 97 + **Step 2: Add rendering block** (after the `identity_input` block from Phase 2) 98 + 99 + ```svelte 100 + {:else if step === 'pds_auth'} 101 + <PdsAuthScreen 102 + pdsUrl={identityInfo!.pdsUrl} 103 + onnext={() => goTo('email_verification')} 104 + onback={() => goTo('identity_input')} 105 + /> 106 + ``` 107 + 108 + **Verification:** 109 + Run: `cd apps/identity-wallet && pnpm check` 110 + Expected: No type errors 111 + 112 + **Commit:** `feat(identity-wallet): wire PdsAuthScreen into page state machine` 113 + <!-- END_TASK_2 --> 114 + <!-- END_SUBCOMPONENT_A --> 115 + 116 + <!-- START_SUBCOMPONENT_B (tasks 3-5) --> 117 + <!-- START_TASK_3 --> 118 + ### Task 3: Create EmailVerificationScreen component 119 + 120 + **Verifies:** plc-key-management.AC5.7, plc-key-management.AC5.8 121 + 122 + **Files:** 123 + - Create: `apps/identity-wallet/src/lib/components/onboarding/EmailVerificationScreen.svelte` 124 + 125 + **Implementation:** 126 + 127 + Create a screen component that sends the verification email on mount and collects the token. 128 + 129 + **Props interface:** 130 + ```typescript 131 + let { 132 + did, 133 + onnext, 134 + onback, 135 + }: { 136 + did: string; 137 + onnext: (result: VerifiedClaimOp) => void; 138 + onback: () => void; 139 + } = $props(); 140 + ``` 141 + 142 + Import `requestClaimVerification`, `signAndVerifyClaim`, `type VerifiedClaimOp`, `type ClaimError` from `$lib/ipc`. 143 + 144 + **Internal state:** 145 + ```typescript 146 + let token = $state(''); 147 + let sending = $state(true); // true while sending verification email 148 + let sendError = $state<string | null>(null); 149 + let verifying = $state(false); // true while verifying token 150 + let verifyError = $state<string | null>(null); 151 + ``` 152 + 153 + **Behavior:** 154 + 155 + 1. **On mount:** Call `requestClaimVerification(did)` to trigger the verification email 156 + - While sending: show spinner + "Sending verification email…" 157 + - On success: show token input form 158 + - On error: show error + "Retry" button that re-calls `requestClaimVerification` 159 + 160 + 2. **Token input form:** (shown after email sent) 161 + - Instruction text: "A verification code has been sent to your email. Enter the code below." 162 + - Text input for `token` (text type, autocomplete off) 163 + - "Verify" button (disabled while `verifying` or when `token` is empty) 164 + 165 + 3. **On verify button press:** Call `signAndVerifyClaim(did, token.trim())` 166 + - While verifying: show spinner or disable button 167 + - On success: call `onnext(result)` with the `VerifiedClaimOp` 168 + - On error: map `ClaimError.code` to user-friendly messages: 169 + - `INVALID_TOKEN` → "Invalid or expired verification code. Check your email and try again." 170 + - `VERIFICATION_FAILED` → "Verification failed: {message}" 171 + - `NETWORK_ERROR` → "Network error. Check your connection and try again." 172 + - Any other → "An error occurred. Please try again." 173 + 174 + 4. **Back button:** calls `onback()` — navigates back to PDS auth 175 + 176 + **Styling:** Follow existing input screen patterns (HandleScreen): centered layout, input with `.error` class on validation failure, `.error-text` for error messages. 177 + 178 + **Verification:** 179 + Run: `cd apps/identity-wallet && pnpm check` 180 + Expected: No type errors 181 + 182 + **Commit:** `feat(identity-wallet): add EmailVerificationScreen component` 183 + <!-- END_TASK_3 --> 184 + 185 + <!-- START_TASK_4 --> 186 + ### Task 4: Wire EmailVerificationScreen into +page.svelte 187 + 188 + **Verifies:** plc-key-management.AC5.7, plc-key-management.AC5.8 189 + 190 + **Files:** 191 + - Modify: `apps/identity-wallet/src/routes/+page.svelte` 192 + 193 + **Implementation:** 194 + 195 + **Step 1: Import EmailVerificationScreen** 196 + ```typescript 197 + import EmailVerificationScreen from '$lib/components/onboarding/EmailVerificationScreen.svelte'; 198 + ``` 199 + 200 + **Step 2: Add rendering block** (after the `pds_auth` block) 201 + 202 + ```svelte 203 + {:else if step === 'email_verification'} 204 + <EmailVerificationScreen 205 + did={identityInfo!.did} 206 + onnext={(result) => { 207 + verifiedClaim = result; 208 + goTo('review_operation'); 209 + }} 210 + onback={() => goTo('pds_auth')} 211 + /> 212 + ``` 213 + 214 + **Verification:** 215 + Run: `cd apps/identity-wallet && pnpm check` 216 + Expected: No type errors 217 + 218 + **Commit:** `feat(identity-wallet): wire EmailVerificationScreen into page state machine` 219 + <!-- END_TASK_4 --> 220 + 221 + <!-- START_TASK_5 --> 222 + ### Task 5: Verify build and existing flow 223 + 224 + **Files:** 225 + - No file changes — verification only 226 + 227 + **Verification:** 228 + Run: `cd apps/identity-wallet && pnpm check` 229 + Expected: No type errors 230 + 231 + Run: `cargo build -p identity-wallet --lib` 232 + Expected: Compiles without errors 233 + 234 + Run: `cargo test -p identity-wallet` 235 + Expected: All existing Rust tests pass 236 + 237 + **Commit:** No commit — verification only 238 + <!-- END_TASK_5 --> 239 + <!-- END_SUBCOMPONENT_B -->
+232
docs/implementation-plans/2026-03-28-plc-key-management/phase_04.md
··· 1 + # Claim Flow Frontend — Phase 4: ReviewOperationScreen + ClaimSuccessScreen 2 + 3 + **Goal:** Create the review screen that displays the PLC operation diff and warnings before submission, and the success screen shown after claim submission. 4 + 5 + **Architecture:** ReviewOperationScreen displays the `OpDiff` from `VerifiedClaimOp`, shows warnings, and on confirm calls `submitClaim()`. ClaimSuccessScreen displays the updated DID document summary and provides navigation to home. Both follow the established component patterns. 6 + 7 + **Tech Stack:** Svelte 5 (runes), TypeScript, Tauri v2 IPC 8 + 9 + **Scope:** 4 of 5 implementation phases 10 + 11 + **Codebase verified:** 2026-03-29 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### plc-key-management.AC5: Import flow frontend 20 + - **plc-key-management.AC5.9 Success:** ReviewOperationScreen displays added/removed keys, changed services, and warnings from VerifiedClaimOp 21 + - **plc-key-management.AC5.10 Success:** User confirms operation, `submitClaim` is called, and success screen shows updated DID document 22 + - **plc-key-management.AC5.11 Failure:** ReviewOperationScreen shows error when `submitClaim` fails (PLC_DIRECTORY_ERROR, NETWORK_ERROR) 23 + - **plc-key-management.AC5.12 Success:** ClaimSuccessScreen displays confirmation with DID doc summary and navigates to home 24 + 25 + --- 26 + 27 + <!-- START_SUBCOMPONENT_A (tasks 1-2) --> 28 + <!-- START_TASK_1 --> 29 + ### Task 1: Create ReviewOperationScreen component 30 + 31 + **Verifies:** plc-key-management.AC5.9, plc-key-management.AC5.10, plc-key-management.AC5.11 32 + 33 + **Files:** 34 + - Create: `apps/identity-wallet/src/lib/components/onboarding/ReviewOperationScreen.svelte` 35 + 36 + **Implementation:** 37 + 38 + Create a screen that displays the operation diff and handles claim submission. 39 + 40 + **Props interface:** 41 + ```typescript 42 + let { 43 + did, 44 + verifiedClaim, 45 + onnext, 46 + oncancel, 47 + }: { 48 + did: string; 49 + verifiedClaim: VerifiedClaimOp; 50 + onnext: (result: ClaimResult) => void; 51 + oncancel: () => void; 52 + } = $props(); 53 + ``` 54 + 55 + Import `submitClaim`, `type VerifiedClaimOp`, `type ClaimResult`, `type ClaimError`, `type OpDiff`, `type ServiceChange`, `type ChangeType` from `$lib/ipc`. 56 + 57 + **Internal state:** 58 + ```typescript 59 + let submitting = $state(false); 60 + let error = $state<string | null>(null); 61 + let warningsAcknowledged = $state(false); 62 + ``` 63 + 64 + **Behavior:** 65 + 66 + 1. **Display OpDiff sections** (from `verifiedClaim.diff`): 67 + 68 + **Keys section:** 69 + - "Keys being added" → list each key in `diff.addedKeys` (green highlight, `+` prefix) 70 + - "Keys being removed" → list each key in `diff.removedKeys` (red highlight, `−` prefix) 71 + - If both arrays are empty, show "No key changes" 72 + 73 + **Services section:** 74 + - For each `ServiceChange` in `diff.changedServices`: 75 + - `changeType === 'added'`: green — "Adding service: {id} → {newEndpoint}" 76 + - `changeType === 'removed'`: red — "Removing service: {id} (was: {oldEndpoint})" 77 + - `changeType === 'modified'`: yellow — "Modifying service: {id}: {oldEndpoint} → {newEndpoint}" 78 + - If array is empty, show "No service changes" 79 + 80 + Display key values truncated for mobile (first 20 chars + "…"), monospace font. Use the `.section` and `.key-card` styling patterns from `DIDDocumentScreen.svelte`. 81 + 82 + 2. **Warnings section (blocks submission per AC5.9):** 83 + - If `verifiedClaim.warnings.length > 0`: display each warning in a yellow/amber highlighted box 84 + - Use icon or colored border to distinguish from regular info 85 + - Add internal state: `let warningsAcknowledged = $state(false);` 86 + - Below the warnings, show a checkbox: "I understand these warnings and want to proceed" 87 + - The "Confirm & Submit" button is **disabled** until `warningsAcknowledged` is `true` 88 + - If no warnings, the checkbox is not shown and `warningsAcknowledged` defaults to effectively `true` (no blocking) 89 + 90 + 3. **Action buttons** (at bottom, `margin-top: auto`): 91 + - "Confirm & Submit" primary button → calls `handleSubmit()` 92 + - Disabled while `submitting` OR when warnings exist and `warningsAcknowledged` is `false` 93 + - "Cancel" secondary button → calls `oncancel()` 94 + - Disabled while `submitting` 95 + 96 + 4. **handleSubmit:** 97 + - Set `submitting = true`, clear `error` 98 + - Call `submitClaim(did)` 99 + - On success: call `onnext(result)` with the `ClaimResult` 100 + - On error: map `ClaimError.code`: 101 + - `PLC_DIRECTORY_ERROR` → "PLC directory rejected the operation: {message}" 102 + - `NETWORK_ERROR` → "Network error. Check your connection and try again." 103 + - `UNAUTHORIZED` → "Authorization expired. Please restart the import flow." 104 + - Any other → "Submission failed. Please try again." 105 + - Set `submitting = false` 106 + 107 + **Styling:** Follow DIDDocumentScreen patterns for section cards. Use color-coded diff entries: 108 + - Added: `#22c55e` (green-500) text/border 109 + - Removed: `#ef4444` (red-500) text/border 110 + - Modified: `#f59e0b` (amber-500) text/border 111 + - Warnings: amber background (`#fffbeb`), amber border (`#f59e0b`) 112 + 113 + **Verification:** 114 + Run: `cd apps/identity-wallet && pnpm check` 115 + Expected: No type errors 116 + 117 + **Commit:** `feat(identity-wallet): add ReviewOperationScreen component` 118 + <!-- END_TASK_1 --> 119 + 120 + <!-- START_TASK_2 --> 121 + ### Task 2: Create ClaimSuccessScreen component 122 + 123 + **Verifies:** plc-key-management.AC5.12 124 + 125 + **Files:** 126 + - Create: `apps/identity-wallet/src/lib/components/onboarding/ClaimSuccessScreen.svelte` 127 + 128 + **Implementation:** 129 + 130 + Create a success screen that shows the updated DID document summary. 131 + 132 + **Props interface:** 133 + ```typescript 134 + let { 135 + claimResult, 136 + ondone, 137 + }: { 138 + claimResult: ClaimResult; 139 + ondone: () => void; 140 + } = $props(); 141 + ``` 142 + 143 + Import `type ClaimResult` from `$lib/ipc`. 144 + 145 + **Behavior:** 146 + 147 + 1. **Success header:** 148 + - Checkmark icon or "✓" in a green circle 149 + - "Identity Claimed Successfully" 150 + - Brief description: "Your rotation key has been updated. You are now in control of this identity." 151 + 152 + 2. **DID document summary** (from `claimResult.updatedDidDoc`): 153 + - Extract `id` (DID), `alsoKnownAs` (handle), `service` (PDS endpoint) from the `Record<string, unknown>` — use the same extraction pattern as `DIDDocumentScreen.svelte` (`Array.isArray()` checks + type assertions) 154 + - Show: DID, handle, PDS endpoint in a summary card 155 + 156 + 3. **"Done" button** → calls `ondone()` — parent navigates to home 157 + 158 + **Styling:** Follow WelcomeScreen-like centered layout. Green checkmark circle, success messaging, summary card using `.section` pattern from DIDDocumentScreen. 159 + 160 + **Verification:** 161 + Run: `cd apps/identity-wallet && pnpm check` 162 + Expected: No type errors 163 + 164 + **Commit:** `feat(identity-wallet): add ClaimSuccessScreen component` 165 + <!-- END_TASK_2 --> 166 + <!-- END_SUBCOMPONENT_A --> 167 + 168 + <!-- START_SUBCOMPONENT_B (tasks 3-4) --> 169 + <!-- START_TASK_3 --> 170 + ### Task 3: Wire ReviewOperationScreen + ClaimSuccessScreen into +page.svelte 171 + 172 + **Verifies:** plc-key-management.AC5.9, plc-key-management.AC5.10, plc-key-management.AC5.11, plc-key-management.AC5.12 173 + 174 + **Files:** 175 + - Modify: `apps/identity-wallet/src/routes/+page.svelte` 176 + 177 + **Implementation:** 178 + 179 + **Step 1: Add imports** 180 + ```typescript 181 + import ReviewOperationScreen from '$lib/components/onboarding/ReviewOperationScreen.svelte'; 182 + import ClaimSuccessScreen from '$lib/components/onboarding/ClaimSuccessScreen.svelte'; 183 + ``` 184 + 185 + **Step 2: Add rendering blocks** (after the `email_verification` block from Phase 3) 186 + 187 + ```svelte 188 + {:else if step === 'review_operation'} 189 + <ReviewOperationScreen 190 + did={identityInfo!.did} 191 + verifiedClaim={verifiedClaim!} 192 + onnext={(result) => { 193 + claimResult = result; 194 + goTo('claim_success'); 195 + }} 196 + oncancel={() => goTo('identity_input')} 197 + /> 198 + {:else if step === 'claim_success'} 199 + <ClaimSuccessScreen 200 + claimResult={claimResult!} 201 + ondone={() => goTo('home')} 202 + /> 203 + ``` 204 + 205 + **Note on `!` assertions:** `verifiedClaim` and `claimResult` are guaranteed non-null at their respective steps because the preceding screens set them before navigating. The `!` assertion is safe here and matches how the codebase handles similar state (e.g., `homeData!` in HomeScreen action handlers). 206 + 207 + **Verification:** 208 + Run: `cd apps/identity-wallet && pnpm check` 209 + Expected: No type errors 210 + 211 + **Commit:** `feat(identity-wallet): wire ReviewOperationScreen and ClaimSuccessScreen into page state machine` 212 + <!-- END_TASK_3 --> 213 + 214 + <!-- START_TASK_4 --> 215 + ### Task 4: Verify full import flow compiles 216 + 217 + **Files:** 218 + - No file changes — verification only 219 + 220 + **Verification:** 221 + Run: `cd apps/identity-wallet && pnpm check` 222 + Expected: No type errors 223 + 224 + Run: `cargo build -p identity-wallet --lib` 225 + Expected: Compiles without errors 226 + 227 + Run: `cargo test -p identity-wallet` 228 + Expected: All existing Rust tests pass 229 + 230 + **Commit:** No commit — verification only 231 + <!-- END_TASK_4 --> 232 + <!-- END_SUBCOMPONENT_B -->
+273
docs/implementation-plans/2026-03-28-plc-key-management/phase_05.md
··· 1 + # Claim Flow Frontend — Phase 5: IdentityListHome 2 + 3 + **Goal:** Replace the single-identity HomeScreen with a multi-identity IdentityListHome that shows all claimed identities with cards, status badges, and a "+" button to add another identity. 4 + 5 + **Architecture:** Adds a `get_stored_did_doc` Tauri command to retrieve per-DID document data from Keychain. IdentityListHome calls `listIdentities()` to get all DIDs, then `getStoredDidDoc(did)` for each to extract handle and PDS info for display. The existing HomeScreen/DIDDocumentScreen/RecoveryInfoScreen remain but are reachable by tapping an identity card. 6 + 7 + **Tech Stack:** Svelte 5 (runes), TypeScript, Tauri v2 IPC, Rust 8 + 9 + **Scope:** 5 of 5 implementation phases 10 + 11 + **Codebase verified:** 2026-03-29 12 + 13 + --- 14 + 15 + ## Acceptance Criteria Coverage 16 + 17 + This phase implements and tests: 18 + 19 + ### plc-key-management.AC5: Import flow frontend 20 + - **plc-key-management.AC5.11 Success:** Multi-identity home shows all claimed identities as cards with rotation key status badges 21 + - **plc-key-management.AC5.12 Success:** "+" button on home navigates back to mode selector to add another identity 22 + - **plc-key-management.AC5.13 Edge:** Existing onboarding flow (create new identity) remains functional and unchanged 23 + 24 + --- 25 + 26 + <!-- START_SUBCOMPONENT_A (tasks 1-2) --> 27 + <!-- START_TASK_1 --> 28 + ### Task 1: Add `get_stored_did_doc` Tauri command and IPC wrapper 29 + 30 + **Verifies:** plc-key-management.AC5.11 (provides data for identity cards with rotation key status) 31 + 32 + **Files:** 33 + - Modify: `apps/identity-wallet/src-tauri/src/lib.rs` (add command + register in invoke_handler) 34 + - Modify: `apps/identity-wallet/src/lib/ipc.ts` (add wrapper) 35 + 36 + **Implementation:** 37 + 38 + **Rust side** — add two `#[tauri::command]` wrappers next to the `list_identities` command added in Phase 1. Note: `serde_json` is already a workspace dependency. 39 + 40 + **Command 1: `get_stored_did_doc`** — wraps `IdentityStore::get_did_doc()` and returns parsed JSON: 41 + 42 + ```rust 43 + #[tauri::command] 44 + fn get_stored_did_doc(did: String) -> Result<Option<serde_json::Value>, identity_store::IdentityStoreError> { 45 + let store = identity_store::IdentityStore; 46 + match store.get_did_doc(&did)? { 47 + Some(json_str) => { 48 + let value: serde_json::Value = serde_json::from_str(&json_str) 49 + .map_err(|e| identity_store::IdentityStoreError::SerializationError { 50 + message: e.to_string(), 51 + })?; 52 + Ok(Some(value)) 53 + } 54 + None => Ok(None), 55 + } 56 + } 57 + ``` 58 + 59 + **Command 2: `get_device_key_id`** — wraps `IdentityStore::get_or_create_device_key()` and returns the `keyId` (did:key URI) for comparing against rotation keys: 60 + 61 + ```rust 62 + #[tauri::command] 63 + fn get_device_key_id(did: String) -> Result<String, identity_store::IdentityStoreError> { 64 + let store = identity_store::IdentityStore; 65 + let device_key = store.get_or_create_device_key(&did)?; 66 + Ok(device_key.key_id) 67 + } 68 + ``` 69 + 70 + `DevicePublicKey` has a `key_id` field (did:key URI, e.g. `did:key:z...`). This is the same value used by `resolve_identity` to check `deviceKeyIsRoot`. 71 + 72 + Register both commands in `tauri::generate_handler![]`. 73 + 74 + Register in `tauri::generate_handler![]` alongside `list_identities`. 75 + 76 + **TypeScript side** — add to `ipc.ts` in the Identity Store section (after `listIdentities`): 77 + 78 + ```typescript 79 + export const getStoredDidDoc = (did: string): Promise<Record<string, unknown> | null> => 80 + invoke('get_stored_did_doc', { did }); 81 + 82 + export const getDeviceKeyId = (did: string): Promise<string> => 83 + invoke('get_device_key_id', { did }); 84 + ``` 85 + 86 + **Verification:** 87 + Run: `cd apps/identity-wallet && pnpm check` 88 + Expected: No type errors 89 + 90 + Run: `cargo build -p identity-wallet --lib` 91 + Expected: Compiles without errors 92 + 93 + **Commit:** `feat(identity-wallet): add get_stored_did_doc Tauri command and IPC wrapper` 94 + <!-- END_TASK_1 --> 95 + 96 + <!-- START_TASK_2 --> 97 + ### Task 2: Create IdentityListHome component 98 + 99 + **Verifies:** plc-key-management.AC5.11, plc-key-management.AC5.12, plc-key-management.AC5.13 100 + 101 + **Files:** 102 + - Create: `apps/identity-wallet/src/lib/components/home/IdentityListHome.svelte` 103 + 104 + **Implementation:** 105 + 106 + Create a multi-identity home screen that loads and displays all claimed identities. 107 + 108 + **Props interface:** 109 + ```typescript 110 + let { 111 + onadd, 112 + onselect, 113 + }: { 114 + onadd: () => void; 115 + onselect: (did: string, didDoc: Record<string, unknown>) => void; 116 + } = $props(); 117 + ``` 118 + 119 + Import `listIdentities`, `getStoredDidDoc`, `getDeviceKeyId` from `$lib/ipc` and `DIDAvatar` from `./DIDAvatar.svelte`. 120 + 121 + **Internal state:** 122 + ```typescript 123 + interface IdentityCard { 124 + did: string; 125 + handle: string | null; 126 + pdsUrl: string | null; 127 + deviceKeyIsRoot: boolean | null; 128 + } 129 + 130 + let identities = $state<IdentityCard[]>([]); 131 + let didDocs = $state<Map<string, Record<string, unknown>>>(new Map()); 132 + let loading = $state(true); 133 + ``` 134 + 135 + **Behavior:** 136 + 137 + 1. **On mount:** Load all identities: 138 + - Call `listIdentities()` to get DIDs 139 + - For each DID, in parallel: 140 + - Call `getStoredDidDoc(did)` to get the DID doc 141 + - Call `getDeviceKeyId(did)` to get the device key's did:key URI 142 + - Extract handle from `alsoKnownAs` (format: `at://{handle}` — extract after `at://`) 143 + - Extract PDS from `service` array (find entry where `id === '#atproto_pds'` or `type === 'AtprotoPersonalDataServer'`, get `serviceEndpoint`) 144 + - Determine `deviceKeyIsRoot`: extract `rotationKeys` array from the DID doc (via `verificationMethod`), check if the device key's did:key URI matches `rotationKeys[0]`. Set `null` if DID doc is missing or rotationKeys is unavailable. 145 + - Build `IdentityCard[]` and cache `didDocs` map for passing to detail views 146 + 147 + 2. **Render identity cards:** 148 + - Each card shows: `DIDAvatar` (reuse existing component), handle (`@{handle}` or "Unknown handle" if null), truncated DID (same truncation as HomeScreen), PDS endpoint 149 + - **Status badge (per AC5.11):** 150 + - `deviceKeyIsRoot === true`: green badge "Root Key" — device key is the primary rotation key 151 + - `deviceKeyIsRoot === false`: amber badge "Not Root" — device key is not the primary rotation key 152 + - `deviceKeyIsRoot === null`: gray badge "Unknown" — could not determine status 153 + - Cards are tappable — `onclick={() => onselect(card.did, didDocs.get(card.did)!)}` 154 + - Use the `.identity-card` styling from HomeScreen as the base pattern, add badge with status-dot pattern from HomeScreen's status indicators 155 + 156 + 3. **"+" button** (floating or at bottom of list): 157 + - "Add Identity" button → calls `onadd()` 158 + - Navigates to mode selector to start a new onboarding or import flow 159 + 160 + 4. **Empty state:** If `identities.length === 0`, show a friendly message ("No identities yet") with the "Add Identity" button 161 + 162 + 5. **Header:** "Identity Wallet" title with refresh button (same pattern as HomeScreen) 163 + 164 + 6. **Refresh:** `loadData()` function that re-fetches all identities, callable from refresh button 165 + 166 + **Styling:** Follow HomeScreen patterns: 167 + - `.screen` container with padding, column layout, gap 168 + - `.header` with title + refresh button 169 + - `.identity-card` cards (background: `#f9fafb`, border: `1px solid #d1d5db`, `12px` radius) 170 + - Cards should be stacked vertically with `0.75rem` gap 171 + - "+" button: `#007aff` primary style, full width at bottom 172 + 173 + **Verification:** 174 + Run: `cd apps/identity-wallet && pnpm check` 175 + Expected: No type errors 176 + 177 + **Commit:** `feat(identity-wallet): add IdentityListHome component` 178 + <!-- END_TASK_2 --> 179 + <!-- END_SUBCOMPONENT_A --> 180 + 181 + <!-- START_SUBCOMPONENT_B (tasks 3-4) --> 182 + <!-- START_TASK_3 --> 183 + ### Task 3: Wire IdentityListHome into +page.svelte 184 + 185 + **Verifies:** plc-key-management.AC5.11, plc-key-management.AC5.12, plc-key-management.AC5.13 186 + 187 + **Files:** 188 + - Modify: `apps/identity-wallet/src/routes/+page.svelte` 189 + 190 + **Implementation:** 191 + 192 + **Step 1: Import IdentityListHome** 193 + ```typescript 194 + import IdentityListHome from '$lib/components/home/IdentityListHome.svelte'; 195 + ``` 196 + 197 + **Step 2: Replace the HomeScreen rendering** for the `home` step 198 + 199 + Currently the `home` step renders `HomeScreen`. Update it to render `IdentityListHome` instead when the user has multiple identities, or keep `HomeScreen` as a detail view when an identity is selected: 200 + 201 + Replace the existing `{:else if step === 'home'}` block. The new `home` step shows `IdentityListHome`: 202 + 203 + ```svelte 204 + {:else if step === 'home'} 205 + <IdentityListHome 206 + onadd={() => goTo('mode_select')} 207 + onselect={(did, didDoc) => { 208 + selectedDid = did; 209 + selectedDidDoc = didDoc; 210 + goTo('identity_detail'); 211 + }} 212 + /> 213 + ``` 214 + 215 + **Step 3: Add `identity_detail` step** (new step for viewing a selected identity) 216 + 217 + Add `'identity_detail'` to the `OnboardingStep` type union. 218 + 219 + Add state variables for the selected identity: 220 + ```typescript 221 + let selectedDid = $state<string | null>(null); 222 + let selectedDidDoc = $state<Record<string, unknown> | null>(null); 223 + ``` 224 + 225 + Add the rendering block — reuse `DIDDocumentScreen` for the detail view: 226 + ```svelte 227 + {:else if step === 'identity_detail'} 228 + <DIDDocumentScreen 229 + didDoc={selectedDidDoc ?? {}} 230 + onback={() => goTo('home')} 231 + /> 232 + ``` 233 + 234 + **Step 4: Update ClaimSuccessScreen navigation** 235 + 236 + In the `claim_success` rendering block, change `ondone` to navigate to `home` (which now shows IdentityListHome): 237 + ```svelte 238 + ondone={() => goTo('home')} 239 + ``` 240 + (This should already be correct from Phase 4.) 241 + 242 + **Verification:** 243 + Run: `cd apps/identity-wallet && pnpm check` 244 + Expected: No type errors 245 + 246 + **Commit:** `feat(identity-wallet): wire IdentityListHome as home screen with identity detail navigation` 247 + <!-- END_TASK_3 --> 248 + 249 + <!-- START_TASK_4 --> 250 + ### Task 4: Final build and flow verification 251 + 252 + **Files:** 253 + - No file changes — verification only 254 + 255 + **Verification:** 256 + Run: `cd apps/identity-wallet && pnpm check` 257 + Expected: No type errors 258 + 259 + Run: `cargo build -p identity-wallet --lib` 260 + Expected: Compiles without errors 261 + 262 + Run: `cargo test -p identity-wallet` 263 + Expected: All existing Rust tests pass 264 + 265 + Verify the complete flow compiles: 266 + 1. mode_select → relay_config → (existing onboarding) → home (IdentityListHome) 267 + 2. mode_select → identity_input → pds_auth → email_verification → review_operation → claim_success → home (IdentityListHome) 268 + 3. home → identity_detail (DIDDocumentScreen) → home 269 + 4. home → "+" → mode_select 270 + 271 + **Commit:** No commit — verification only 272 + <!-- END_TASK_4 --> 273 + <!-- END_SUBCOMPONENT_B -->
+425
docs/implementation-plans/2026-03-28-plc-key-management/test-requirements.md
··· 1 + # plc-key-management.AC5: Test Requirements 2 + 3 + **Feature:** plc-key-management -- Import Flow Frontend (AC5) 4 + **Design plan:** `docs/design-plans/2026-03-28-plc-key-management.md` 5 + **Implementation plan:** `docs/implementation-plans/2026-03-28-plc-key-management/` (Phases 1--5) 6 + **Last verified:** 2026-03-29 7 + 8 + --- 9 + 10 + ## Acceptance Criteria Index 11 + 12 + Every AC5 acceptance criterion from the design plan, mapped to its implementing phase and test strategy. 13 + 14 + ### plc-key-management.AC5: Import flow frontend 15 + 16 + | ID | Criterion | Phase | Test Strategy | 17 + |----|-----------|-------|---------------| 18 + | plc-key-management.AC5.1 | Mode selector on first launch shows "Create new identity" and "I have an identity" options | Phase 1 | Human Verification | 19 + | plc-key-management.AC5.2 | App skips mode selector and goes to home when `listIdentities()` returns non-empty | Phase 1 | Human Verification | 20 + | plc-key-management.AC5.3 | Identity input screen resolves a handle and displays current PDS + rotation key state | Phase 2 | Human Verification | 21 + | plc-key-management.AC5.4 | Identity input screen shows inline error for unresolvable handle | Phase 2 | Human Verification | 22 + | plc-key-management.AC5.5 | PDS auth screen triggers OAuth and proceeds after `auth_ready` event | Phase 3 | Human Verification | 23 + | plc-key-management.AC5.6 | Email verification screen sends token and shows verified operation diff | Phase 3 | Human Verification | 24 + | plc-key-management.AC5.7 | Email verification screen shows inline error for invalid token and stays on same screen | Phase 3 | Human Verification | 25 + | plc-key-management.AC5.8 | Review operation screen displays added/removed keys and changed services clearly | Phase 4 | Human Verification | 26 + | plc-key-management.AC5.9 | Review operation screen blocks submission and shows warning when verification detects suspicious changes | Phase 4 | Human Verification | 27 + | plc-key-management.AC5.10 | Claim success screen shows updated DID doc and navigates to home | Phase 4 | Human Verification | 28 + | plc-key-management.AC5.11 | Multi-identity home shows all claimed identities as cards with rotation key status badges | Phase 5 | Human Verification | 29 + | plc-key-management.AC5.12 | "+" button on home navigates back to mode selector to add another identity | Phase 5 | Human Verification | 30 + | plc-key-management.AC5.13 | Existing onboarding flow (create new identity) remains functional and unchanged | Phase 1, 5 | Human Verification | 31 + 32 + --- 33 + 34 + ## Automated Test Coverage 35 + 36 + No new automated tests are required for AC5. The three Tauri commands introduced across these phases (`list_identities`, `get_stored_did_doc`, `get_device_key_id`) are thin wrappers around `IdentityStore` methods that are already covered by unit tests in `apps/identity-wallet/src-tauri/src/identity_store.rs`. The claim flow IPC commands (`resolveIdentity`, `startPdsAuth`, `requestClaimVerification`, `signAndVerifyClaim`, `submitClaim`) are tested by their respective AC groups (AC1--AC4) and are not retested at the frontend integration layer. 37 + 38 + All frontend changes are Svelte 5 screen components rendered inside a Tauri WKWebView on iOS. The project has no browser-based component test harness (no Vitest, Playwright, or similar), so UI behavior is verified manually on the iOS Simulator. 39 + 40 + **Existing automated tests that support AC5 (run with `cargo test -p identity-wallet`):** 41 + 42 + | Method | Test File | Verifies | 43 + |--------|-----------|----------| 44 + | `IdentityStore::list_identities()` | `identity_store.rs` | Round-trip of managed-dids array; empty and populated cases | 45 + | `IdentityStore::get_did_doc()` | `identity_store.rs` | DID doc storage and retrieval; `None` for missing docs | 46 + | `IdentityStore::get_or_create_device_key()` | `identity_store.rs` | Per-DID key generation; idempotency; cross-DID isolation | 47 + | `resolve_identity()` | `claim.rs` | Identity resolution with PDS discovery and rotation key extraction | 48 + | `sign_and_verify_claim()` | `claim.rs` | Claim signing, diff generation, warning population, verification failures | 49 + | `submit_claim()` | `claim.rs` | PLC directory submission and identity persistence | 50 + 51 + --- 52 + 53 + ## Human Verification Required 54 + 55 + All 13 AC5 criteria require human verification on the iOS Simulator. These criteria cover UI rendering, screen transitions, user interaction, and state machine wiring that cannot be exercised without the Tauri runtime and WKWebView. 56 + 57 + ### Prerequisites 58 + 59 + - macOS with Xcode installed 60 + - iOS Simulator available (iPhone target) 61 + - `cargo tauri ios dev` running successfully from `apps/identity-wallet/` 62 + - An existing AT Protocol identity on a reachable PDS (e.g., a Bluesky account) for the import flow 63 + - Access to the email address associated with that identity (for verification code) 64 + - `cargo test -p identity-wallet` passing (all existing Rust tests green) 65 + 66 + --- 67 + 68 + ### plc-key-management.AC5.1: Mode selector on first launch 69 + 70 + **Criterion:** Mode selector on first launch shows "Create new identity" and "I have an identity" options. 71 + 72 + **Why manual:** UI rendering in WKWebView; requires fresh app state with no Keychain entries. 73 + 74 + **Steps:** 75 + 1. Reset the iOS Simulator (Device > Erase All Content and Settings). 76 + 2. Launch the app via `cd apps/identity-wallet && cargo tauri ios dev`. 77 + 3. **Verify:** The first screen displayed is the mode selector (not the relay config screen). 78 + 4. **Verify:** The screen shows the heading "Identity Wallet" and tagline "Your self-sovereign identity, in your pocket." 79 + 5. **Verify:** Two buttons are visible: "Create new identity" (primary) and "I have an identity" (secondary). 80 + 6. **Verify:** Both buttons are tappable and not disabled. 81 + 82 + --- 83 + 84 + ### plc-key-management.AC5.2: App skips mode selector when identities exist 85 + 86 + **Criterion:** App skips mode selector and goes to home when `listIdentities()` returns non-empty. 87 + 88 + **Why manual:** Requires Keychain state from a previously completed claim or identity creation flow. The `listIdentities()` Tauri command reads from the iOS Keychain, which is not available in `cargo test`. 89 + 90 + **Steps:** 91 + 1. Start from a state where at least one identity has been claimed or created (complete the full onboarding or import flow first). 92 + 2. Force-quit the app (swipe up from app switcher or stop the dev server). 93 + 3. Relaunch the app via `cargo tauri ios dev`. 94 + 4. **Verify:** The mode selector screen does NOT appear. 95 + 5. **Verify:** The app navigates directly to the home screen (IdentityListHome showing identity cards). 96 + 97 + --- 98 + 99 + ### plc-key-management.AC5.3: Identity input screen resolves a handle 100 + 101 + **Criterion:** Identity input screen resolves a handle and displays current PDS + rotation key state. 102 + 103 + **Why manual:** Requires network call to resolve a real handle via DNS/HTTP and fetch the DID document from plc.directory, plus visual inspection of the resolved identity card. 104 + 105 + **Steps:** 106 + 1. From the mode selector (AC5.1 state), tap "I have an identity". 107 + 2. **Verify:** The identity input screen appears with a text input, "Resolve" button, and "Back" button. 108 + 3. Enter a valid AT Protocol handle (e.g., your Bluesky handle like `yourname.bsky.social`). 109 + 4. Tap "Resolve". 110 + 5. **Verify:** A loading state appears while resolution is in progress. 111 + 6. **Verify:** On success, an identity info card appears showing: 112 + - Handle: `@yourname.bsky.social` 113 + - DID: truncated `did:plc:...` 114 + - PDS URL: the PDS endpoint (e.g., `https://morel.us-east.host.bsky.network`) 115 + - Rotation key status: either "Your device is the root key" (green) or "Device key is not the root key" (neutral) 116 + 7. **Verify:** A "Continue" button appears below the identity card. 117 + 8. Tap "Back" and verify navigation returns to the mode selector. 118 + 119 + --- 120 + 121 + ### plc-key-management.AC5.4: Identity input screen shows error for unresolvable handle 122 + 123 + **Criterion:** Identity input screen shows inline error for unresolvable handle. 124 + 125 + **Why manual:** Requires network call that fails, plus visual inspection of error display and screen retention. 126 + 127 + **Steps:** 128 + 1. From the mode selector, tap "I have an identity". 129 + 2. Enter a handle that does not exist (e.g., `this-handle-definitely-does-not-exist-12345.test`). 130 + 3. Tap "Resolve". 131 + 4. **Verify:** A loading state appears briefly. 132 + 5. **Verify:** An inline error message appears: "Handle not found. Check the spelling and try again." 133 + 6. **Verify:** The user remains on the identity input screen (no navigation occurred). 134 + 7. **Verify:** The "Continue" button does NOT appear (no resolved identity). 135 + 8. Clear the input, enter a valid handle, and tap "Resolve". 136 + 9. **Verify:** The error clears and the identity info card appears (recovery works). 137 + 138 + --- 139 + 140 + ### plc-key-management.AC5.5: PDS auth screen triggers OAuth and proceeds 141 + 142 + **Criterion:** PDS auth screen triggers OAuth and proceeds after `auth_ready` event. 143 + 144 + **Why manual:** Requires PDS OAuth flow via Safari, deep-link callback routing through iOS, and visual observation of screen transitions. 145 + 146 + **Steps:** 147 + 1. Complete AC5.3 (resolve a handle successfully). 148 + 2. Tap "Continue" on the identity input screen. 149 + 3. **Verify:** The PDS auth screen appears, showing the PDS URL and an "Authenticate with PDS" button. 150 + 4. **Verify:** A "Back" button is visible to return to identity input. 151 + 5. Tap "Authenticate with PDS". 152 + 6. **Verify:** A spinner appears with text "Opening browser for PDS authentication..." 153 + 7. **Verify:** Safari opens with the PDS OAuth authorization page. 154 + 8. Complete the OAuth authorization in Safari (approve the request). 155 + 9. **Verify:** Safari redirects back to the app via deep link. 156 + 10. **Verify:** The app advances to the email verification screen. 157 + 158 + --- 159 + 160 + ### plc-key-management.AC5.6: Email verification screen sends token and shows verified operation diff 161 + 162 + **Criterion:** Email verification screen sends token and shows verified operation diff. 163 + 164 + **Why manual:** Requires a live PDS to send the verification email, user interaction to enter the token received via email, and visual inspection of the verified operation diff display. 165 + 166 + **Steps:** 167 + 1. Complete AC5.5 (PDS auth succeeded, now on the email verification screen). 168 + 2. **Verify:** On mount, a spinner appears with "Sending verification email..." 169 + 3. **Verify:** After the email is sent, the screen shows: instruction text ("A verification code has been sent to your email..."), a text input for the token, and a "Verify" button. 170 + 4. Check your email for the verification code from the PDS. 171 + 5. Enter the verification code in the token input field. 172 + 6. Tap "Verify". 173 + 7. **Verify:** A loading state appears while verification is in progress. 174 + 8. **Verify:** On success, the app navigates to the review operation screen (AC5.8). 175 + 176 + --- 177 + 178 + ### plc-key-management.AC5.7: Email verification screen shows error for invalid token 179 + 180 + **Criterion:** Email verification screen shows inline error for invalid token and stays on same screen. 181 + 182 + **Why manual:** Requires a live PDS to reject an invalid token, plus visual inspection of error display and screen retention. 183 + 184 + **Steps:** 185 + 1. Complete AC5.5 (PDS auth succeeded, now on the email verification screen). 186 + 2. Wait for the "Sending verification email..." step to complete. 187 + 3. Enter an obviously invalid token (e.g., `000000` or `invalid`). 188 + 4. Tap "Verify". 189 + 5. **Verify:** An inline error message appears: "Invalid or expired verification code. Check your email and try again." 190 + 6. **Verify:** The user remains on the email verification screen (no navigation occurred). 191 + 7. **Verify:** The token input field is still editable. 192 + 8. Clear the input, enter the correct token from your email, and tap "Verify". 193 + 9. **Verify:** The error clears and the flow advances to the review operation screen (recovery works). 194 + 195 + --- 196 + 197 + ### plc-key-management.AC5.8: Review operation screen displays operation diff clearly 198 + 199 + **Criterion:** Review operation screen displays added/removed keys and changed services clearly. 200 + 201 + **Why manual:** Requires visual inspection of the color-coded diff display (green for added, red for removed, yellow for modified) and layout of the operation summary. 202 + 203 + **Steps:** 204 + 1. Complete AC5.6 (email verification succeeded, now on the review operation screen). 205 + 2. **Verify:** The review screen displays the following sections: 206 + - **Keys section:** Shows keys being added (green, `+` prefix -- this should include your device's key) and keys being removed (red, `-` prefix), or "No key changes" if none. 207 + - **Services section:** Shows service changes (added/removed/modified) or "No service changes" if none. 208 + 3. **Verify:** Key values are displayed in monospace font, truncated for mobile (first 20 characters + "..."). 209 + 4. **Verify:** Color coding is correct: added items in green (`#22c55e`), removed items in red (`#ef4444`), modified items in amber (`#f59e0b`). 210 + 5. **Verify:** A "Confirm & Submit" primary button and "Cancel" secondary button are visible at the bottom. 211 + 212 + --- 213 + 214 + ### plc-key-management.AC5.9: Review operation screen blocks submission on warnings 215 + 216 + **Criterion:** Review operation screen blocks submission and shows warning when verification detects suspicious changes. 217 + 218 + **Why manual:** Requires a claim operation that produces warnings. This scenario is difficult to trigger with a production PDS, so this may require a controlled test environment where the PDS returns an operation with unexpected changes. 219 + 220 + **Steps (if warnings are present in the operation):** 221 + 1. Arrive at the review operation screen with a `VerifiedClaimOp` that contains warnings (non-empty `warnings` array). 222 + 2. **Verify:** Warning messages are displayed in amber/yellow highlighted boxes with distinct styling from regular info. 223 + 3. **Verify:** The "Confirm & Submit" button is disabled (grayed out, not tappable). 224 + 4. **Verify:** A checkbox appears below the warnings: "I understand these warnings and want to proceed." 225 + 5. Tap the checkbox to acknowledge the warnings. 226 + 6. **Verify:** The "Confirm & Submit" button becomes enabled (tappable). 227 + 7. Untap the checkbox. 228 + 8. **Verify:** The button becomes disabled again. 229 + 230 + **Steps (if no warnings are present):** 231 + 1. Arrive at the review operation screen with a `VerifiedClaimOp` that has an empty `warnings` array. 232 + 2. **Verify:** No warning section or checkbox is displayed. 233 + 3. **Verify:** The "Confirm & Submit" button is enabled by default (not blocked). 234 + 235 + **Note:** Both paths should be tested. If a controlled PDS environment is not available to produce warnings, verify the no-warnings path on a production PDS and visually inspect the code to confirm the warnings path is correctly wired. 236 + 237 + --- 238 + 239 + ### plc-key-management.AC5.10: Claim success screen shows updated DID doc and navigates to home 240 + 241 + **Criterion:** Claim success screen shows updated DID doc and navigates to home. 242 + 243 + **Why manual:** Requires the full claim submission to plc.directory to succeed, then visual inspection of the success screen and navigation to the home screen. 244 + 245 + **Steps:** 246 + 1. Complete AC5.8 (on the review operation screen with no warnings, or acknowledge warnings per AC5.9). 247 + 2. Tap "Confirm & Submit". 248 + 3. **Verify:** A loading state appears while the claim is being submitted to plc.directory. 249 + 4. **Verify:** On success, the claim success screen appears with: 250 + - A green checkmark icon or circle 251 + - Heading: "Identity Claimed Successfully" 252 + - Description text about rotation key control 253 + - A DID document summary card showing: DID, handle, and PDS endpoint 254 + 5. **Verify:** A "Done" button is visible. 255 + 6. Tap "Done". 256 + 7. **Verify:** The app navigates to the home screen (IdentityListHome). 257 + 8. **Verify:** The claimed identity appears as a card on the home screen. 258 + 259 + --- 260 + 261 + ### plc-key-management.AC5.11: Multi-identity home shows identity cards with status badges 262 + 263 + **Criterion:** Multi-identity home shows all claimed identities as cards with rotation key status badges. 264 + 265 + **Why manual:** Requires multiple identities in the Keychain (from both onboarding and import flows), plus visual inspection of identity cards with rotation key status badges. 266 + 267 + **Steps:** 268 + 1. Claim at least two identities (one via "Create new identity" and one via "I have an identity", or two via import). 269 + 2. Navigate to or relaunch to the home screen. 270 + 3. **Verify:** The home screen (IdentityListHome) displays one card per identity. 271 + 4. **Verify:** Each card shows: 272 + - A DID avatar 273 + - Handle (e.g., `@yourname.bsky.social`) or "Unknown handle" if unavailable 274 + - Truncated DID 275 + - PDS endpoint 276 + 5. **Verify:** Each card has a rotation key status badge: 277 + - Green "Root Key" badge if the device key is the primary rotation key (`rotationKeys[0]`) 278 + - Amber "Not Root" badge if the device key is not the primary rotation key 279 + - Gray "Unknown" badge if status could not be determined 280 + 6. **Verify:** Tapping a card navigates to the identity detail view (DIDDocumentScreen) for that identity. 281 + 7. **Verify:** The detail view shows the full DID document with a "Back" button that returns to the home screen. 282 + 283 + --- 284 + 285 + ### plc-key-management.AC5.12: "+" button navigates to mode selector 286 + 287 + **Criterion:** "+" button on home navigates back to mode selector to add another identity. 288 + 289 + **Why manual:** Requires visual inspection of the "+" button and navigation to the mode selector. 290 + 291 + **Steps:** 292 + 1. From the home screen (IdentityListHome) with at least one identity. 293 + 2. **Verify:** An "Add Identity" button is visible at the bottom of the identity list. 294 + 3. Tap the "Add Identity" button. 295 + 4. **Verify:** The app navigates to the mode selector screen. 296 + 5. **Verify:** Both options ("Create new identity" and "I have an identity") are available. 297 + 6. Tap "I have an identity" and begin a second import flow. 298 + 7. **Verify:** The import flow works correctly (the previous identity is not affected). 299 + 8. After completing the second import, verify the home screen shows both identities. 300 + 301 + --- 302 + 303 + ### plc-key-management.AC5.13: Existing onboarding flow remains functional 304 + 305 + **Criterion:** Existing onboarding flow (create new identity) remains functional and unchanged. 306 + 307 + **Why manual:** Regression test requiring the full onboarding flow through all existing screens, verifying no behavioral changes from the import flow additions. 308 + 309 + **Steps:** 310 + 1. Reset the iOS Simulator (Erase All Content and Settings). 311 + 2. Launch the app via `cargo tauri ios dev`. 312 + 3. **Verify:** The mode selector screen appears (AC5.1). 313 + 4. Tap "Create new identity". 314 + 5. **Verify:** The relay config screen appears (or is skipped if a relay URL is already saved). 315 + 6. Enter a valid relay URL (e.g., `https://relay.ezpds.com` or `http://localhost:2583`) and tap Connect. 316 + 7. **Verify:** The welcome screen appears with the "Get Started" button. 317 + 8. Proceed through the full onboarding flow: 318 + - Welcome > Claim Code > Email > Handle > Password > Loading > DID Ceremony > DID Success > Shamir Backup > Handle Registration > Complete > Authenticating 319 + 9. **Verify:** Each screen renders correctly with proper inputs, buttons, and transitions. 320 + 10. **Verify:** The onboarding completes and the app arrives at the home screen (IdentityListHome). 321 + 11. **Verify:** The newly created identity appears as a card on the home screen with the correct handle and DID. 322 + 12. Force-quit and relaunch the app. 323 + 13. **Verify:** The app opens directly to the home screen (skips mode selector per AC5.2). 324 + 325 + --- 326 + 327 + ## End-to-End Scenarios 328 + 329 + ### E2E-1: First launch -- import existing identity 330 + 331 + | Step | Action | Expected | 332 + |------|--------|----------| 333 + | 1 | Reset the iOS Simulator | Simulator is clean | 334 + | 2 | Launch the app | Mode selector appears with two options | 335 + | 3 | Tap "I have an identity" | Identity input screen appears | 336 + | 4 | Enter a valid handle and tap "Resolve" | Identity info card displays DID, handle, PDS, rotation key status | 337 + | 5 | Tap "Continue" | PDS auth screen appears with PDS URL | 338 + | 6 | Tap "Authenticate with PDS" | Safari opens for OAuth | 339 + | 7 | Complete OAuth in Safari | App returns to email verification screen | 340 + | 8 | Wait for "Sending verification email..." to complete | Token input form appears | 341 + | 9 | Enter verification code from email, tap "Verify" | Review operation screen appears with diff | 342 + | 10 | Tap "Confirm & Submit" (acknowledge warnings if present) | Claim success screen appears with DID doc summary | 343 + | 11 | Tap "Done" | Home screen shows the claimed identity card with status badge | 344 + 345 + ### E2E-2: Multiple identities -- create then import 346 + 347 + | Step | Action | Expected | 348 + |------|--------|----------| 349 + | 1 | Reset the iOS Simulator | Simulator is clean | 350 + | 2 | Launch the app, tap "Create new identity" | Relay config screen appears | 351 + | 3 | Complete full onboarding (relay config through auth) | Home screen shows one identity card | 352 + | 4 | Tap "Add Identity" button | Mode selector appears | 353 + | 5 | Tap "I have an identity" | Identity input screen appears | 354 + | 6 | Complete import flow (steps 4--10 from E2E-1) | Claim success screen appears | 355 + | 7 | Tap "Done" | Home screen shows two identity cards with status badges | 356 + | 8 | Force-quit and relaunch | Home screen appears directly with both cards | 357 + 358 + ### E2E-3: Import flow error recovery 359 + 360 + | Step | Action | Expected | 361 + |------|--------|----------| 362 + | 1 | From mode selector, tap "I have an identity" | Identity input screen appears | 363 + | 2 | Enter an invalid handle, tap "Resolve" | Inline error: "Handle not found..." | 364 + | 3 | Clear input, enter a valid handle, tap "Resolve" | Identity card appears; error clears | 365 + | 4 | Tap "Continue", then tap "Authenticate with PDS" | Safari opens | 366 + | 5 | Cancel or deny OAuth in Safari | Error message on PDS auth screen | 367 + | 6 | Tap "Authenticate with PDS" again | Safari reopens for retry | 368 + | 7 | Complete OAuth successfully | Email verification screen appears | 369 + | 8 | Enter wrong verification code, tap "Verify" | Inline error: "Invalid or expired verification code..." | 370 + | 9 | Enter correct code, tap "Verify" | Review operation screen appears; error clears | 371 + | 10 | Tap "Cancel" on review screen | Returns to identity input screen | 372 + 373 + ### E2E-4: Returning user skips mode selector 374 + 375 + | Step | Action | Expected | 376 + |------|--------|----------| 377 + | 1 | Complete E2E-1 (at least one identity claimed) | Home screen visible | 378 + | 2 | Force-quit the app completely | App terminated | 379 + | 3 | Relaunch the app | Home screen appears directly (mode selector skipped) | 380 + | 4 | Verify identity cards are displayed | All previously claimed identities shown with correct data | 381 + 382 + --- 383 + 384 + ## Traceability Matrix 385 + 386 + | Acceptance Criterion | Automated Test | Manual Step | 387 + |----------------------|----------------|-------------| 388 + | plc-key-management.AC5.1 | -- | Mode selector with two buttons on fresh launch | 389 + | plc-key-management.AC5.2 | `IdentityStore::list_identities()` tests (underlying method) | Relaunch with existing identities skips to home | 390 + | plc-key-management.AC5.3 | `resolve_identity()` tests (underlying method) | Enter handle, tap Resolve, verify identity card | 391 + | plc-key-management.AC5.4 | `resolve_identity()` error path tests (underlying method) | Enter bad handle, verify inline error, verify screen retention | 392 + | plc-key-management.AC5.5 | `start_pds_auth()` tests (underlying method) | Tap Authenticate, verify Safari opens, complete OAuth | 393 + | plc-key-management.AC5.6 | `sign_and_verify_claim()` tests (underlying method) | Enter correct token, verify flow advances | 394 + | plc-key-management.AC5.7 | `sign_and_verify_claim()` INVALID_TOKEN test (underlying method) | Enter wrong token, verify inline error, verify screen retention | 395 + | plc-key-management.AC5.8 | -- | Inspect diff display: added/removed keys, changed services, color coding | 396 + | plc-key-management.AC5.9 | -- | Verify button disabled with warnings, checkbox enables it | 397 + | plc-key-management.AC5.10 | `submit_claim()` tests (underlying method) | Verify success screen content, tap Done, verify home navigation | 398 + | plc-key-management.AC5.11 | `IdentityStore::get_did_doc()` + `get_or_create_device_key()` tests | Verify multi-identity cards with rotation key status badges | 399 + | plc-key-management.AC5.12 | -- | Tap "+" button, verify mode selector appears | 400 + | plc-key-management.AC5.13 | -- | Full onboarding regression: mode selector > create > relay > onboarding > home | 401 + 402 + --- 403 + 404 + ## Summary 405 + 406 + - **Total acceptance criteria:** 13 (AC5.1 through AC5.13) 407 + - **Automated test coverage:** 0 new tests required (all Tauri commands are thin wrappers around already-tested `IdentityStore` and claim module methods) 408 + - **Human verification required:** 13 criteria (all are UI-level behaviors requiring iOS Simulator) 409 + - **End-to-end scenarios:** 4 410 + 411 + ### Test Execution Commands 412 + 413 + ```bash 414 + # Verify all existing Rust backend tests pass (prerequisite for human verification) 415 + cargo test -p identity-wallet 416 + 417 + # Verify TypeScript types compile 418 + cd apps/identity-wallet && pnpm check 419 + 420 + # Verify Rust compiles 421 + cargo build -p identity-wallet --lib 422 + 423 + # Launch for manual testing 424 + cd apps/identity-wallet && cargo tauri ios dev 425 + ```