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.

feat: implement DIDDocumentScreen component with structured view and raw JSON toggle

- Renders structured sections for DID document fields: id, alsoKnownAs, verificationMethod, service
- Gracefully handles missing/empty arrays (displays nothing if array is absent or empty)
- Copy button for publicKeyMultibase values with 2-second 'Copied\!' feedback
- Raw JSON toggle reveals full DID document as formatted monospace text
- Back button returns to home screen
- Verifies: MM-150.AC3.5, MM-150.AC3.6, MM-150.AC3.7, MM-150.AC3.9

authored by

Malpercio and committed by
Tangled
bd77f95a 99365fde

+276 -1
+276 -1
apps/identity-wallet/src/lib/components/home/DIDDocumentScreen.svelte
··· 6 6 didDoc: Record<string, unknown>; 7 7 onback: () => void; 8 8 } = $props(); 9 + 10 + let showRaw = $state(false); 11 + let copiedKeyId = $state<string | null>(null); 12 + 13 + // Extract typed arrays from the loosely-typed didDoc. 14 + let verificationMethods = $derived( 15 + Array.isArray(didDoc.verificationMethod) 16 + ? (didDoc.verificationMethod as Array<Record<string, unknown>>) 17 + : [] 18 + ); 19 + 20 + let alsoKnownAs = $derived( 21 + Array.isArray(didDoc.alsoKnownAs) 22 + ? (didDoc.alsoKnownAs as Array<string>) 23 + : [] 24 + ); 25 + 26 + let services = $derived( 27 + Array.isArray(didDoc.service) 28 + ? (didDoc.service as Array<Record<string, unknown>>) 29 + : [] 30 + ); 31 + 32 + let rawJson = $derived(JSON.stringify(didDoc, null, 2)); 33 + 34 + async function copyKey(keyId: string, value: string) { 35 + try { 36 + await navigator.clipboard.writeText(value); 37 + copiedKeyId = keyId; 38 + setTimeout(() => { copiedKeyId = null; }, 2000); 39 + } catch (e) { 40 + console.error('clipboard write failed:', e); 41 + } 42 + } 9 43 </script> 10 44 11 - <div>DIDDocumentScreen stub — replaced by Phase 5</div> 45 + <div class="screen"> 46 + <div class="header"> 47 + <button class="back-btn" onclick={onback} aria-label="Back">‹ Back</button> 48 + <h2 class="title">DID Document</h2> 49 + </div> 50 + 51 + <!-- Identity section --> 52 + <div class="section"> 53 + <p class="section-label">Identifier</p> 54 + <p class="mono-value">{didDoc.id ?? '—'}</p> 55 + </div> 56 + 57 + <!-- alsoKnownAs --> 58 + {#if alsoKnownAs.length > 0} 59 + <div class="section"> 60 + <p class="section-label">Also Known As</p> 61 + {#each alsoKnownAs as alias} 62 + <p class="mono-value">{alias}</p> 63 + {/each} 64 + </div> 65 + {/if} 66 + 67 + <!-- Verification Methods --> 68 + {#if verificationMethods.length > 0} 69 + <div class="section"> 70 + <p class="section-label">Verification Keys</p> 71 + {#each verificationMethods as method} 72 + <div class="key-card"> 73 + <p class="key-type">{method.type ?? 'Unknown'}</p> 74 + <p class="key-id">{method.id}</p> 75 + {#if method.publicKeyMultibase} 76 + <div class="key-value-row"> 77 + <code class="key-value">{String(method.publicKeyMultibase).slice(0, 20)}…</code> 78 + <button 79 + class="copy-btn" 80 + onclick={() => copyKey(String(method.id), String(method.publicKeyMultibase))} 81 + > 82 + {copiedKeyId === String(method.id) ? 'Copied!' : 'Copy'} 83 + </button> 84 + </div> 85 + {/if} 86 + </div> 87 + {/each} 88 + </div> 89 + {/if} 90 + 91 + <!-- Services --> 92 + {#if services.length > 0} 93 + <div class="section"> 94 + <p class="section-label">Services</p> 95 + {#each services as svc} 96 + <div class="service-card"> 97 + <p class="service-type">{svc.type ?? 'Unknown'}</p> 98 + <p class="service-endpoint">{svc.serviceEndpoint}</p> 99 + </div> 100 + {/each} 101 + </div> 102 + {/if} 103 + 104 + <!-- Raw JSON toggle --> 105 + <button 106 + class="toggle-btn" 107 + onclick={() => { showRaw = !showRaw; }} 108 + > 109 + {showRaw ? 'Hide Raw JSON' : 'Show Raw JSON'} 110 + </button> 111 + 112 + {#if showRaw} 113 + <pre class="raw-json">{rawJson}</pre> 114 + {/if} 115 + </div> 116 + 117 + <style> 118 + .screen { 119 + display: flex; 120 + flex-direction: column; 121 + height: 100%; 122 + padding: 2rem 1.5rem; 123 + gap: 1.25rem; 124 + overflow-y: auto; 125 + } 126 + 127 + .header { 128 + display: flex; 129 + align-items: center; 130 + gap: 0.75rem; 131 + } 132 + 133 + .back-btn { 134 + background: none; 135 + border: none; 136 + font-size: 1rem; 137 + color: #007aff; 138 + cursor: pointer; 139 + padding: 0; 140 + font-weight: 500; 141 + white-space: nowrap; 142 + } 143 + 144 + .title { 145 + font-size: 1.2rem; 146 + font-weight: 700; 147 + color: #111827; 148 + margin: 0; 149 + } 150 + 151 + .section { 152 + background: #f9fafb; 153 + border: 1px solid #d1d5db; 154 + border-radius: 12px; 155 + padding: 1rem 1.25rem; 156 + display: flex; 157 + flex-direction: column; 158 + gap: 0.5rem; 159 + } 160 + 161 + .section-label { 162 + font-size: 0.75rem; 163 + font-weight: 600; 164 + color: #6b7280; 165 + margin: 0; 166 + text-transform: uppercase; 167 + letter-spacing: 0.05em; 168 + } 169 + 170 + .mono-value { 171 + font-family: monospace; 172 + font-size: 0.8rem; 173 + color: #374151; 174 + margin: 0; 175 + word-break: break-all; 176 + } 177 + 178 + .key-card { 179 + background: #fff; 180 + border: 1px solid #e5e7eb; 181 + border-radius: 8px; 182 + padding: 0.75rem; 183 + display: flex; 184 + flex-direction: column; 185 + gap: 0.25rem; 186 + } 187 + 188 + .key-type { 189 + font-size: 0.8rem; 190 + font-weight: 600; 191 + color: #374151; 192 + margin: 0; 193 + } 194 + 195 + .key-id { 196 + font-family: monospace; 197 + font-size: 0.75rem; 198 + color: #6b7280; 199 + margin: 0; 200 + word-break: break-all; 201 + } 202 + 203 + .key-value-row { 204 + display: flex; 205 + align-items: center; 206 + gap: 0.5rem; 207 + margin-top: 0.25rem; 208 + } 209 + 210 + .key-value { 211 + font-family: monospace; 212 + font-size: 0.75rem; 213 + color: #374151; 214 + background: #f3f4f6; 215 + padding: 0.2rem 0.4rem; 216 + border-radius: 4px; 217 + flex: 1; 218 + min-width: 0; 219 + overflow: hidden; 220 + text-overflow: ellipsis; 221 + white-space: nowrap; 222 + } 223 + 224 + .copy-btn { 225 + background: #007aff; 226 + color: #fff; 227 + border: none; 228 + border-radius: 6px; 229 + padding: 0.3rem 0.75rem; 230 + font-size: 0.8rem; 231 + font-weight: 600; 232 + cursor: pointer; 233 + white-space: nowrap; 234 + flex-shrink: 0; 235 + } 236 + 237 + .service-card { 238 + background: #fff; 239 + border: 1px solid #e5e7eb; 240 + border-radius: 8px; 241 + padding: 0.75rem; 242 + display: flex; 243 + flex-direction: column; 244 + gap: 0.25rem; 245 + } 246 + 247 + .service-type { 248 + font-size: 0.8rem; 249 + font-weight: 600; 250 + color: #374151; 251 + margin: 0; 252 + } 253 + 254 + .service-endpoint { 255 + font-family: monospace; 256 + font-size: 0.8rem; 257 + color: #6b7280; 258 + margin: 0; 259 + word-break: break-all; 260 + } 261 + 262 + .toggle-btn { 263 + background: none; 264 + border: 1px solid #d1d5db; 265 + border-radius: 8px; 266 + padding: 0.6rem 1rem; 267 + font-size: 0.9rem; 268 + color: #374151; 269 + cursor: pointer; 270 + text-align: center; 271 + } 272 + 273 + .raw-json { 274 + background: #f3f4f6; 275 + border: 1px solid #d1d5db; 276 + border-radius: 8px; 277 + padding: 1rem; 278 + font-family: monospace; 279 + font-size: 0.75rem; 280 + color: #374151; 281 + overflow-x: auto; 282 + white-space: pre; 283 + word-break: normal; 284 + margin: 0; 285 + } 286 + </style>