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 final code review feedback for mobile claim flow

- Fix ClaimSuccessScreen pdsEndpoint extraction: Use PLC directory format (services map with atproto_pds.endpoint) instead of W3C format (service array with serviceEndpoint). Previously always showed '--'.

- Fix DIDDocumentScreen format mismatch: Add normalizePlcDocToW3c() function in +page.svelte to convert stored PLC format DID documents (services map, rotationKeys array) to W3C array format (service array, verificationMethod array) before passing to DIDDocumentScreen for viewing stored identities.

- Fix IdentityListHome badge visibility: Remove conditional that hid badge when deviceKeyIsRoot is null. The badge with 'Unknown' label and gray styling already exists in getBadgeLabel and CSS, so badge should always be shown.

- Create shared PDS extraction utility: Extract duplicate PDS extraction logic into new did-doc-utils.ts with extractPdsFromPlcDoc() function. Use it in both IdentityListHome and ClaimSuccessScreen to reduce code duplication.

All changes verified: build passes, type check passes (0 errors, 0 warnings).

authored by

Malpercio and committed by
Tangled
d1be546a f45ba15c

+95 -32
+11 -17
apps/identity-wallet/src/lib/components/home/IdentityListHome.svelte
··· 1 1 <script lang="ts"> 2 2 import { onMount } from 'svelte'; 3 3 import { listIdentities, getStoredDidDoc, getDeviceKeyId } from '$lib/ipc'; 4 + import { extractPdsFromPlcDoc } from '$lib/did-doc-utils'; 4 5 import DIDAvatar from './DIDAvatar.svelte'; 5 6 6 7 let { ··· 42 43 } 43 44 44 45 function extractPds(didDoc: Record<string, unknown>): string | null { 45 - const services = didDoc.services; 46 - if (typeof services !== 'object' || services === null) return null; 47 - const pds = (services as Record<string, unknown>).atproto_pds; 48 - if (typeof pds !== 'object' || pds === null) return null; 49 - const endpoint = (pds as Record<string, unknown>).endpoint; 50 - return typeof endpoint === 'string' ? endpoint : null; 46 + return extractPdsFromPlcDoc(didDoc); 51 47 } 52 48 53 49 function isDeviceKeyRoot( ··· 150 146 </div> 151 147 </div> 152 148 <div class="card-badge"> 153 - {#if card.deviceKeyIsRoot !== null} 154 - <span 155 - class="badge" 156 - class:badge--root={card.deviceKeyIsRoot === true} 157 - class:badge--not-root={card.deviceKeyIsRoot === false} 158 - class:badge--unknown={card.deviceKeyIsRoot === null} 159 - > 160 - <span class="badge-dot"></span> 161 - {getBadgeLabel(card.deviceKeyIsRoot)} 162 - </span> 163 - {/if} 149 + <span 150 + class="badge" 151 + class:badge--root={card.deviceKeyIsRoot === true} 152 + class:badge--not-root={card.deviceKeyIsRoot === false} 153 + class:badge--unknown={card.deviceKeyIsRoot === null} 154 + > 155 + <span class="badge-dot"></span> 156 + {getBadgeLabel(card.deviceKeyIsRoot)} 157 + </span> 164 158 </div> 165 159 </button> 166 160 {/each}
+5 -14
apps/identity-wallet/src/lib/components/onboarding/ClaimSuccessScreen.svelte
··· 1 1 <script lang="ts"> 2 2 import { type ClaimResult } from '$lib/ipc'; 3 + import { extractPdsFromPlcDoc } from '$lib/did-doc-utils'; 3 4 4 5 let { 5 6 claimResult, ··· 23 24 ); 24 25 25 26 let pdsEndpoint = $derived.by(() => { 26 - const services = claimResult.updatedDidDoc?.service; 27 - if (!Array.isArray(services)) return '—'; 28 - 29 - const pdsService = services.find( 30 - (svc) => 31 - typeof svc === 'object' && 32 - svc !== null && 33 - (svc as Record<string, unknown>)?.type === 'AtprotoPersonalDataServer' 34 - ); 27 + const doc = claimResult.updatedDidDoc; 28 + if (typeof doc !== 'object' || doc === null) return '—'; 35 29 36 - if (pdsService && typeof (pdsService as Record<string, unknown>)?.serviceEndpoint === 'string') { 37 - return (pdsService as Record<string, unknown>).serviceEndpoint as string; 38 - } 39 - 40 - return '—'; 30 + const endpoint = extractPdsFromPlcDoc(doc as Record<string, unknown>); 31 + return endpoint ?? '—'; 41 32 }); 42 33 </script> 43 34
+23
apps/identity-wallet/src/lib/did-doc-utils.ts
··· 1 + /** 2 + * Utility functions for working with DID documents, especially PLC directory format. 3 + */ 4 + 5 + /** 6 + * Extracts the PDS (Personal Data Server) endpoint from a PLC directory format DID document. 7 + * 8 + * PLC documents store services as a map with keys like "atproto_pds", where the value 9 + * has an "endpoint" field containing the PDS URL. 10 + * 11 + * @param doc - The DID document (loosely typed Record) 12 + * @returns The PDS endpoint URL, or null if not found or invalid 13 + */ 14 + export function extractPdsFromPlcDoc(doc: Record<string, unknown>): string | null { 15 + const services = doc.services; 16 + if (typeof services !== 'object' || services === null) return null; 17 + 18 + const pds = (services as Record<string, unknown>).atproto_pds; 19 + if (typeof pds !== 'object' || pds === null) return null; 20 + 21 + const endpoint = (pds as Record<string, unknown>).endpoint; 22 + return typeof endpoint === 'string' ? endpoint : null; 23 + }
+56 -1
apps/identity-wallet/src/routes/+page.svelte
··· 92 92 step = next; 93 93 } 94 94 95 + /** 96 + * Normalizes a PLC directory format DID document to W3C format. 97 + * Converts the PLC map-based format (services, verificationMethods as objects) 98 + * to the W3C array-based format (service, verificationMethod as arrays). 99 + */ 100 + function normalizePlcDocToW3c(plcDoc: Record<string, unknown>): Record<string, unknown> { 101 + const normalized: Record<string, unknown> = { 102 + id: plcDoc.id, 103 + alsoKnownAs: plcDoc.alsoKnownAs, 104 + }; 105 + 106 + // Convert services map to service array 107 + if (typeof plcDoc.services === 'object' && plcDoc.services !== null) { 108 + const servicesMap = plcDoc.services as Record<string, unknown>; 109 + const serviceArray: Array<Record<string, unknown>> = []; 110 + 111 + for (const [key, value] of Object.entries(servicesMap)) { 112 + if (typeof value === 'object' && value !== null) { 113 + const serviceObj = value as Record<string, unknown>; 114 + serviceArray.push({ 115 + id: `#${key}`, 116 + type: serviceObj.type, 117 + serviceEndpoint: serviceObj.endpoint, 118 + }); 119 + } 120 + } 121 + 122 + if (serviceArray.length > 0) { 123 + normalized.service = serviceArray; 124 + } 125 + } 126 + 127 + // Convert rotationKeys array to verificationMethod array 128 + if (Array.isArray(plcDoc.rotationKeys)) { 129 + const verificationMethods: Array<Record<string, unknown>> = []; 130 + 131 + for (let i = 0; i < plcDoc.rotationKeys.length; i++) { 132 + const key = plcDoc.rotationKeys[i]; 133 + if (typeof key === 'string') { 134 + verificationMethods.push({ 135 + id: `#rotation-${i}`, 136 + type: 'Multikey', 137 + publicKeyMultibase: key, 138 + }); 139 + } 140 + } 141 + 142 + if (verificationMethods.length > 0) { 143 + normalized.verificationMethod = verificationMethods; 144 + } 145 + } 146 + 147 + return normalized; 148 + } 149 + 95 150 // ── Relay configuration and OAuth event listener ────────────────────── 96 151 97 152 onMount(async () => { ··· 318 373 319 374 {:else if step === 'identity_detail'} 320 375 <DIDDocumentScreen 321 - didDoc={selectedDidDoc ?? {}} 376 + didDoc={selectedDidDoc ? normalizePlcDocToW3c(selectedDidDoc) : {}} 322 377 onback={() => goTo('home')} 323 378 /> 324 379