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(identity-wallet): add ReviewOperationScreen component

Implements a screen that displays the operation diff (added/removed keys and
service changes) and handles claim submission. Shows warnings that must be
acknowledged before proceeding. Maps ClaimError codes to user-facing messages.
Verifies plc-key-management.AC5.9, AC5.10, AC5.11.

authored by

Malpercio and committed by
Tangled
114d2b9a d0ae8f4e

+361
+361
apps/identity-wallet/src/lib/components/onboarding/ReviewOperationScreen.svelte
··· 1 + <script lang="ts"> 2 + import { submitClaim, type VerifiedClaimOp, type ClaimResult, type ClaimError } from '$lib/ipc'; 3 + 4 + let { 5 + did, 6 + verifiedClaim, 7 + onnext, 8 + oncancel, 9 + }: { 10 + did: string; 11 + verifiedClaim: VerifiedClaimOp; 12 + onnext: (result: ClaimResult) => void; 13 + oncancel: () => void; 14 + } = $props(); 15 + 16 + let submitting = $state(false); 17 + let error = $state<string | null>(null); 18 + let warningsAcknowledged = $state(false); 19 + 20 + async function handleSubmit() { 21 + submitting = true; 22 + error = null; 23 + 24 + try { 25 + const result = await submitClaim(did); 26 + onnext(result); 27 + } catch (raw: unknown) { 28 + if ( 29 + typeof raw === 'object' && 30 + raw !== null && 31 + 'code' in raw && 32 + typeof (raw as ClaimError).code === 'string' 33 + ) { 34 + const err = raw as ClaimError; 35 + switch (err.code) { 36 + case 'PLC_DIRECTORY_ERROR': 37 + error = `PLC directory rejected the operation: ${err.message || 'unknown error'}`; 38 + break; 39 + case 'NETWORK_ERROR': 40 + error = 'Network error. Check your connection and try again.'; 41 + break; 42 + case 'UNAUTHORIZED': 43 + error = 'Authorization expired. Please restart the import flow.'; 44 + break; 45 + default: 46 + error = 'Submission failed. Please try again.'; 47 + } 48 + } else { 49 + error = 'Submission failed. Please try again.'; 50 + } 51 + submitting = false; 52 + } 53 + } 54 + 55 + </script> 56 + 57 + <div class="screen"> 58 + <div class="header"> 59 + <h2 class="title">Review Operation</h2> 60 + </div> 61 + 62 + <!-- Keys section --> 63 + <div class="section"> 64 + <p class="section-label">Keys</p> 65 + {#if verifiedClaim.diff.addedKeys.length > 0 || verifiedClaim.diff.removedKeys.length > 0} 66 + {#if verifiedClaim.diff.addedKeys.length > 0} 67 + <div class="subsection-label">Keys being added</div> 68 + {#each verifiedClaim.diff.addedKeys as key} 69 + <div class="diff-entry added"> 70 + <span class="diff-prefix">+</span> 71 + <code class="diff-value">{key.slice(0, 20)}…</code> 72 + </div> 73 + {/each} 74 + {/if} 75 + 76 + {#if verifiedClaim.diff.removedKeys.length > 0} 77 + <div class="subsection-label">Keys being removed</div> 78 + {#each verifiedClaim.diff.removedKeys as key} 79 + <div class="diff-entry removed"> 80 + <span class="diff-prefix">−</span> 81 + <code class="diff-value">{key.slice(0, 20)}…</code> 82 + </div> 83 + {/each} 84 + {/if} 85 + {:else} 86 + <p class="no-changes">No key changes</p> 87 + {/if} 88 + </div> 89 + 90 + <!-- Services section --> 91 + <div class="section"> 92 + <p class="section-label">Services</p> 93 + {#if verifiedClaim.diff.changedServices.length > 0} 94 + {#each verifiedClaim.diff.changedServices as service} 95 + {#if service.changeType === 'added'} 96 + <div class="diff-entry added"> 97 + <span class="diff-prefix">+</span> 98 + <span class="service-text">Adding service: {service.id} → {service.newEndpoint}</span> 99 + </div> 100 + {:else if service.changeType === 'removed'} 101 + <div class="diff-entry removed"> 102 + <span class="diff-prefix">−</span> 103 + <span class="service-text">Removing service: {service.id} (was: {service.oldEndpoint})</span> 104 + </div> 105 + {:else if service.changeType === 'modified'} 106 + <div class="diff-entry modified"> 107 + <span class="diff-prefix">~</span> 108 + <span class="service-text">Modifying service: {service.id}: {service.oldEndpoint} → {service.newEndpoint}</span> 109 + </div> 110 + {/if} 111 + {/each} 112 + {:else} 113 + <p class="no-changes">No service changes</p> 114 + {/if} 115 + </div> 116 + 117 + <!-- Warnings section --> 118 + {#if verifiedClaim.warnings.length > 0} 119 + <div class="warnings-section"> 120 + <p class="section-label">Warnings</p> 121 + {#each verifiedClaim.warnings as warning} 122 + <div class="warning-box"> 123 + <p class="warning-text">{warning}</p> 124 + </div> 125 + {/each} 126 + <label class="checkbox-label"> 127 + <input 128 + type="checkbox" 129 + bind:checked={warningsAcknowledged} 130 + disabled={submitting} 131 + /> 132 + <span>I understand these warnings and want to proceed</span> 133 + </label> 134 + </div> 135 + {/if} 136 + 137 + <!-- Error display --> 138 + {#if error} 139 + <div class="error-box"> 140 + <p class="error-text">{error}</p> 141 + </div> 142 + {/if} 143 + 144 + <!-- Action buttons --> 145 + <div class="button-group"> 146 + <button 147 + class="cta cta--primary" 148 + onclick={handleSubmit} 149 + disabled={submitting || (verifiedClaim.warnings.length > 0 && !warningsAcknowledged)} 150 + > 151 + {submitting ? 'Submitting…' : 'Confirm & Submit'} 152 + </button> 153 + <button 154 + class="cta cta--secondary" 155 + onclick={oncancel} 156 + disabled={submitting} 157 + > 158 + Cancel 159 + </button> 160 + </div> 161 + </div> 162 + 163 + <style> 164 + .screen { 165 + display: flex; 166 + flex-direction: column; 167 + height: 100%; 168 + padding: 2rem 1.5rem; 169 + gap: 1.25rem; 170 + overflow-y: auto; 171 + } 172 + 173 + .header { 174 + display: flex; 175 + align-items: center; 176 + gap: 0.75rem; 177 + } 178 + 179 + .title { 180 + font-size: 1.2rem; 181 + font-weight: 700; 182 + color: #111827; 183 + margin: 0; 184 + } 185 + 186 + .section { 187 + background: #f9fafb; 188 + border: 1px solid #d1d5db; 189 + border-radius: 12px; 190 + padding: 1rem 1.25rem; 191 + display: flex; 192 + flex-direction: column; 193 + gap: 0.5rem; 194 + } 195 + 196 + .section-label { 197 + font-size: 0.75rem; 198 + font-weight: 600; 199 + color: #6b7280; 200 + margin: 0; 201 + text-transform: uppercase; 202 + letter-spacing: 0.05em; 203 + } 204 + 205 + .subsection-label { 206 + font-size: 0.8rem; 207 + font-weight: 600; 208 + color: #374151; 209 + margin: 0.5rem 0 0.25rem 0; 210 + } 211 + 212 + .no-changes { 213 + font-size: 0.85rem; 214 + color: #6b7280; 215 + margin: 0.5rem 0 0; 216 + font-style: italic; 217 + } 218 + 219 + .diff-entry { 220 + display: flex; 221 + align-items: flex-start; 222 + gap: 0.5rem; 223 + padding: 0.5rem; 224 + border-radius: 8px; 225 + margin: 0.25rem 0; 226 + font-size: 0.85rem; 227 + } 228 + 229 + .diff-entry.added { 230 + background: rgba(34, 197, 94, 0.1); 231 + border-left: 3px solid #22c55e; 232 + color: #166534; 233 + } 234 + 235 + .diff-entry.removed { 236 + background: rgba(239, 68, 68, 0.1); 237 + border-left: 3px solid #ef4444; 238 + color: #7f1d1d; 239 + } 240 + 241 + .diff-entry.modified { 242 + background: rgba(245, 158, 11, 0.1); 243 + border-left: 3px solid #f59e0b; 244 + color: #92400e; 245 + } 246 + 247 + .diff-prefix { 248 + font-weight: 600; 249 + flex-shrink: 0; 250 + width: 1rem; 251 + } 252 + 253 + .diff-value { 254 + font-family: monospace; 255 + font-size: 0.75rem; 256 + word-break: break-all; 257 + margin: 0; 258 + } 259 + 260 + .service-text { 261 + word-break: break-word; 262 + } 263 + 264 + .warnings-section { 265 + background: #fffbeb; 266 + border: 1px solid #f59e0b; 267 + border-radius: 12px; 268 + padding: 1rem 1.25rem; 269 + display: flex; 270 + flex-direction: column; 271 + gap: 0.75rem; 272 + } 273 + 274 + .warning-box { 275 + background: #fff; 276 + border-left: 3px solid #f59e0b; 277 + border-radius: 4px; 278 + padding: 0.75rem; 279 + display: flex; 280 + flex-direction: column; 281 + gap: 0; 282 + } 283 + 284 + .warning-text { 285 + font-size: 0.85rem; 286 + color: #92400e; 287 + margin: 0; 288 + line-height: 1.4; 289 + } 290 + 291 + .checkbox-label { 292 + display: flex; 293 + align-items: center; 294 + gap: 0.5rem; 295 + font-size: 0.85rem; 296 + color: #374151; 297 + cursor: pointer; 298 + margin-top: 0.5rem; 299 + } 300 + 301 + .checkbox-label input { 302 + cursor: pointer; 303 + accent-color: #f59e0b; 304 + } 305 + 306 + .checkbox-label span { 307 + user-select: none; 308 + } 309 + 310 + .error-box { 311 + background: rgba(239, 68, 68, 0.1); 312 + border: 1px solid #ef4444; 313 + border-radius: 8px; 314 + padding: 0.75rem 1rem; 315 + } 316 + 317 + .error-text { 318 + font-size: 0.85rem; 319 + color: #7f1d1d; 320 + margin: 0; 321 + line-height: 1.4; 322 + } 323 + 324 + .button-group { 325 + display: flex; 326 + flex-direction: column; 327 + gap: 0.75rem; 328 + margin-top: auto; 329 + } 330 + 331 + .cta { 332 + padding: 1rem; 333 + border: none; 334 + border-radius: 12px; 335 + font-size: 1rem; 336 + font-weight: 600; 337 + cursor: pointer; 338 + transition: opacity 0.2s; 339 + width: 100%; 340 + } 341 + 342 + .cta--primary { 343 + background: #007aff; 344 + color: #fff; 345 + } 346 + 347 + .cta--primary:disabled { 348 + opacity: 0.5; 349 + cursor: not-allowed; 350 + } 351 + 352 + .cta--secondary { 353 + background: #e5e7eb; 354 + color: #374151; 355 + } 356 + 357 + .cta--secondary:disabled { 358 + opacity: 0.5; 359 + cursor: not-allowed; 360 + } 361 + </style>