Graphical PDS migrator for AT Protocol
1import { useState } from "preact/hooks";
2import { Link } from "../components/Link.tsx";
3
4interface PlcUpdateStep {
5 name: string;
6 status: "pending" | "in-progress" | "verifying" | "completed" | "error";
7 error?: string;
8}
9
10// Content chunks for the description
11const contentChunks = [
12 {
13 title: "Welcome to Key Management",
14 subtitle: "BOARDING PASS - SECTION A",
15 content: (
16 <>
17 <div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
18 GATE: KEY-01 • SEAT: DID-1A
19 </div>
20 <p class="text-slate-700 dark:text-slate-300 mb-4">
21 This tool helps you add a new rotation key to your{" "}
22 <Link
23 href="https://web.plc.directory/"
24 isExternal
25 class="text-blue-600 dark:text-blue-400"
26 >
27 PLC (Public Ledger of Credentials)
28 </Link>
29 . Having control of a rotation key gives you sovereignty over your DID
30 (Decentralized Identifier).
31 </p>
32 </>
33 ),
34 },
35 {
36 title: "Key Benefits",
37 subtitle: "BOARDING PASS - SECTION B",
38 content: (
39 <>
40 <div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
41 GATE: KEY-02 • SEAT: DID-1B
42 </div>
43 <div class="space-y-4">
44 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
45 <h4 class="font-mono font-bold text-amber-500 dark:text-amber-400 mb-2">
46 PROVIDER MOBILITY ✈️
47 </h4>
48 <p class="text-slate-700 dark:text-slate-300">
49 Change your PDS without losing your identity, protecting you if
50 your provider becomes hostile.
51 </p>
52 </div>
53 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
54 <h4 class="font-mono font-bold text-amber-500 dark:text-amber-400 mb-2">
55 IDENTITY CONTROL ✨
56 </h4>
57 <p class="text-slate-700 dark:text-slate-300">
58 Modify your DID document independently of your provider.
59 </p>
60 </div>
61 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
62 <p class="text-slate-700 dark:text-slate-300">
63 💡 It's good practice to have a rotation key so you can move to a
64 different provider if you need to.
65 </p>
66 </div>
67 </div>
68 </>
69 ),
70 },
71 {
72 title: "⚠️ CRITICAL SECURITY WARNING",
73 subtitle: "BOARDING PASS - SECTION C",
74 content: (
75 <>
76 <div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
77 GATE: KEY-03 • SEAT: DID-1C
78 </div>
79 <div class="p-4 bg-red-50 dark:bg-red-900 rounded-lg border-2 border-red-500 dark:border-red-700 mb-4">
80 <div class="flex items-center mb-3">
81 <span class="text-2xl mr-2">⚠️</span>
82 <h4 class="font-mono font-bold text-red-700 dark:text-red-400 text-lg">
83 NON-REVOCABLE KEY WARNING
84 </h4>
85 </div>
86 <div class="space-y-3 text-red-700 dark:text-red-300">
87 <p class="font-bold">
88 This rotation key CANNOT BE DISABLED OR DELETED once added:
89 </p>
90 <ul class="list-disc pl-5 space-y-2">
91 <li>
92 If compromised, the attacker can take complete control of your
93 account and identity
94 </li>
95 <li>
96 Malicious actors with this key have COMPLETE CONTROL of your
97 account and identity
98 </li>
99 <li>
100 Store securely, like a password (e.g. <strong>DO NOT</strong>
101 {" "}
102 keep it in Notes or any easily accessible app on an unlocked
103 device).
104 </li>
105 </ul>
106 </div>
107 </div>
108 <div class="p-3 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
109 <p class="text-slate-700 dark:text-slate-300">
110 💡 We recommend adding a custom rotation key but recommend{" "}
111 <strong class="italic">against</strong>{" "}
112 having more than one custom rotation key, as more than one increases
113 risk.
114 </p>
115 </div>
116 </>
117 ),
118 },
119 {
120 title: "Technical Overview",
121 subtitle: "BOARDING PASS - SECTION C",
122 content: (
123 <>
124 <div class="passenger-info text-slate-600 dark:text-slate-300 font-mono text-sm mb-4">
125 GATE: KEY-03 • SEAT: DID-1C
126 </div>
127 <div class="p-4 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
128 <div class="flex items-center mb-3">
129 <span class="text-lg mr-2">📝</span>
130 <h4 class="font-mono font-bold text-amber-500 dark:text-amber-400">
131 TECHNICAL DETAILS
132 </h4>
133 </div>
134 <p class="text-slate-700 dark:text-slate-300">
135 The rotation key is a did:key that will be added to your PLC
136 document's rotationKeys array. This process uses the AT Protocol's
137 PLC operations to update your DID document.
138 <Link
139 href="https://web.plc.directory/"
140 class="block ml-1 text-blue-600 dark:text-blue-400"
141 isExternal
142 >
143 Learn more about did:plc
144 </Link>
145 </p>
146 </div>
147 </>
148 ),
149 },
150];
151
152export default function PlcUpdateProgress() {
153 const [hasStarted, setHasStarted] = useState(false);
154 const [currentChunkIndex, setCurrentChunkIndex] = useState(0);
155 const [steps, setSteps] = useState<PlcUpdateStep[]>([
156 { name: "Generate Rotation Key", status: "pending" },
157 { name: "Start PLC update", status: "pending" },
158 { name: "Complete PLC update", status: "pending" },
159 ]);
160 const [generatedKey, setGeneratedKey] = useState<string>("");
161 const [keyJson, setKeyJson] = useState<any>(null);
162 const [emailToken, setEmailToken] = useState<string>("");
163 const [hasDownloadedKey, setHasDownloadedKey] = useState(false);
164 const [downloadedKeyId, setDownloadedKeyId] = useState<string | null>(null);
165 const [hasContinuedPastDownload, setHasContinuedPastDownload] = useState(
166 false,
167 );
168
169 const updateStepStatus = (
170 index: number,
171 status: PlcUpdateStep["status"],
172 error?: string,
173 ) => {
174 console.log(
175 `Updating step ${index} to ${status}${
176 error ? ` with error: ${error}` : ""
177 }`,
178 );
179 setSteps((prevSteps) =>
180 prevSteps.map((step, i) =>
181 i === index
182 ? { ...step, status, error }
183 : i > index
184 ? { ...step, status: "pending", error: undefined }
185 : step
186 )
187 );
188 };
189
190 const handleStart = () => {
191 setHasStarted(true);
192 // Automatically start the first step
193 setTimeout(() => {
194 handleGenerateKey();
195 }, 100);
196 };
197
198 const getStepDisplayName = (step: PlcUpdateStep, index: number) => {
199 if (step.status === "completed") {
200 switch (index) {
201 case 0:
202 return "Rotation Key Generated";
203 case 1:
204 return "PLC Operation Requested";
205 case 2:
206 return "PLC Update Completed";
207 }
208 }
209
210 if (step.status === "in-progress") {
211 switch (index) {
212 case 0:
213 return "Generating Rotation Key...";
214 case 1:
215 return "Requesting PLC Operation Token...";
216 case 2:
217 return step.name ===
218 "Enter the code sent to your email to complete PLC update"
219 ? step.name
220 : "Completing PLC Update...";
221 }
222 }
223
224 if (step.status === "verifying") {
225 switch (index) {
226 case 0:
227 return "Verifying Rotation Key Generation...";
228 case 1:
229 return "Verifying PLC Operation Token Request...";
230 case 2:
231 return "Verifying PLC Update Completion...";
232 }
233 }
234
235 return step.name;
236 };
237
238 const handleStartPlcUpdate = async (keyToUse?: string) => {
239 const key = keyToUse || generatedKey;
240
241 // Debug logging
242 console.log("=== PLC Update Debug ===");
243 console.log("Current state:", {
244 keyToUse,
245 generatedKey,
246 key,
247 hasKeyJson: !!keyJson,
248 keyJsonId: keyJson?.publicKeyDid,
249 hasDownloadedKey,
250 downloadedKeyId,
251 steps: steps.map((s) => ({ name: s.name, status: s.status })),
252 });
253
254 if (!key) {
255 console.log("No key generated yet");
256 updateStepStatus(1, "error", "No key generated yet");
257 return;
258 }
259
260 if (!keyJson || keyJson.publicKeyDid !== key) {
261 console.log("Key mismatch or missing:", {
262 hasKeyJson: !!keyJson,
263 keyJsonId: keyJson?.publicKeyDid,
264 expectedKey: key,
265 });
266 updateStepStatus(
267 1,
268 "error",
269 "Please ensure you have the correct key loaded",
270 );
271 return;
272 }
273
274 updateStepStatus(1, "in-progress");
275 try {
276 // First request the token
277 console.log("Requesting PLC token...");
278 const tokenRes = await fetch("/api/plc/token", {
279 method: "GET",
280 });
281 const tokenText = await tokenRes.text();
282 console.log("Token response:", tokenText);
283
284 if (!tokenRes.ok) {
285 try {
286 const json = JSON.parse(tokenText);
287 throw new Error(json.message || "Failed to request PLC token");
288 } catch {
289 throw new Error(tokenText || "Failed to request PLC token");
290 }
291 }
292
293 let data;
294 try {
295 data = JSON.parse(tokenText);
296 if (!data.success) {
297 throw new Error(data.message || "Failed to request token");
298 }
299 } catch {
300 throw new Error("Invalid response from server");
301 }
302
303 console.log("Token request successful, updating UI...");
304 // Update step name to prompt for token
305 setSteps((prevSteps) =>
306 prevSteps.map((step, i) =>
307 i === 1
308 ? {
309 ...step,
310 name: "Enter the code sent to your email to complete PLC update",
311 status: "in-progress",
312 }
313 : step
314 )
315 );
316 } catch (error) {
317 console.error("Token request failed:", error);
318 updateStepStatus(
319 1,
320 "error",
321 error instanceof Error ? error.message : String(error),
322 );
323 }
324 };
325
326 const handleTokenSubmit = async () => {
327 console.log("=== Token Submit Debug ===");
328 console.log("Current state:", {
329 emailToken,
330 generatedKey,
331 keyJsonId: keyJson?.publicKeyDid,
332 steps: steps.map((s) => ({ name: s.name, status: s.status })),
333 });
334
335 if (!emailToken) {
336 console.log("No token provided");
337 updateStepStatus(1, "error", "Please enter the email token");
338 return;
339 }
340
341 if (!keyJson || !keyJson.publicKeyDid) {
342 console.log("Missing key data");
343 updateStepStatus(1, "error", "Key data is missing, please try again");
344 return;
345 }
346
347 // Prevent duplicate submissions
348 if (steps[1].status === "completed" || steps[2].status === "completed") {
349 console.log("Update already completed, preventing duplicate submission");
350 return;
351 }
352
353 updateStepStatus(1, "completed");
354 try {
355 updateStepStatus(2, "in-progress");
356 console.log("Submitting update request with token...");
357 // Send the update request with both key and token
358 const res = await fetch("/api/plc/update", {
359 method: "POST",
360 headers: { "Content-Type": "application/json" },
361 body: JSON.stringify({
362 key: keyJson.publicKeyDid,
363 token: emailToken,
364 }),
365 });
366 const text = await res.text();
367 console.log("Update response:", text);
368
369 let data;
370 try {
371 data = JSON.parse(text);
372 } catch {
373 throw new Error("Invalid response from server");
374 }
375
376 // Check for error responses
377 if (!res.ok || !data.success) {
378 const errorMessage = data.message || "Failed to complete PLC update";
379 console.error("Update failed:", errorMessage);
380 throw new Error(errorMessage);
381 }
382
383 // Only proceed if we have a successful response
384 console.log("Update completed successfully!");
385
386 // Add a delay before marking steps as completed for better UX
387 updateStepStatus(2, "verifying");
388
389 const verifyRes = await fetch("/api/plc/verify", {
390 method: "POST",
391 headers: { "Content-Type": "application/json" },
392 body: JSON.stringify({
393 key: keyJson.publicKeyDid,
394 }),
395 });
396
397 const verifyText = await verifyRes.text();
398 console.log("Verification response:", verifyText);
399
400 let verifyData;
401 try {
402 verifyData = JSON.parse(verifyText);
403 } catch {
404 throw new Error("Invalid verification response from server");
405 }
406
407 if (!verifyRes.ok || !verifyData.success) {
408 const errorMessage = verifyData.message ||
409 "Failed to verify PLC update";
410 console.error("Verification failed:", errorMessage);
411 throw new Error(errorMessage);
412 }
413
414 console.log("Verification successful, marking steps as completed");
415 updateStepStatus(2, "completed");
416 } catch (error) {
417 console.error("Update failed:", error);
418 // Reset the steps to error state
419 updateStepStatus(
420 1,
421 "error",
422 error instanceof Error ? error.message : String(error),
423 );
424 updateStepStatus(2, "pending"); // Reset the final step
425
426 // If token is invalid, we should clear it so user can try again
427 if (
428 error instanceof Error &&
429 error.message.toLowerCase().includes("token is invalid")
430 ) {
431 setEmailToken("");
432 }
433 }
434 };
435
436 const handleDownload = () => {
437 console.log("=== Download Debug ===");
438 console.log("Download started with:", {
439 hasKeyJson: !!keyJson,
440 keyJsonId: keyJson?.publicKeyDid,
441 });
442
443 if (!keyJson) {
444 console.error("No key JSON to download");
445 return;
446 }
447
448 try {
449 const jsonString = JSON.stringify(keyJson, null, 2);
450 const filename = `plc-key-${keyJson.publicKeyDid || "unknown"}.json`;
451
452 // Create data URL
453 const dataStr = "data:text/json;charset=utf-8," +
454 encodeURIComponent(jsonString);
455
456 // Create download link
457 const downloadAnchorNode = document.createElement("a");
458 downloadAnchorNode.setAttribute("href", dataStr);
459 downloadAnchorNode.setAttribute("download", filename);
460
461 // For Chrome/Firefox compatibility
462 downloadAnchorNode.style.display = "none";
463 document.body.appendChild(downloadAnchorNode);
464
465 // Trigger download
466 downloadAnchorNode.click();
467
468 // Cleanup
469 document.body.removeChild(downloadAnchorNode);
470
471 console.log("Download completed, showing continue button...");
472 setHasDownloadedKey(true);
473 setDownloadedKeyId(keyJson.publicKeyDid);
474 // Keep step 0 in completed state but don't auto-proceed
475 } catch (error) {
476 console.error("Download failed:", error);
477 }
478 };
479
480 const handleGenerateKey = async () => {
481 console.log("=== Generate Key Debug ===");
482 updateStepStatus(0, "in-progress");
483 setKeyJson(null);
484 setGeneratedKey("");
485 setHasDownloadedKey(false);
486 setDownloadedKeyId(null);
487
488 try {
489 console.log("Requesting new key...");
490 const res = await fetch("/api/plc/keys");
491 const text = await res.text();
492 console.log("Key generation response:", text);
493
494 if (!res.ok) {
495 try {
496 const json = JSON.parse(text);
497 throw new Error(json.message || "Failed to generate key");
498 } catch {
499 throw new Error(text || "Failed to generate key");
500 }
501 }
502
503 let data;
504 try {
505 data = JSON.parse(text);
506 } catch {
507 throw new Error("Invalid response from /api/plc/keys");
508 }
509
510 if (!data.publicKeyDid || !data.privateKeyHex) {
511 throw new Error("Key generation failed: missing key data");
512 }
513
514 console.log("Key generated successfully:", {
515 keyId: data.publicKeyDid,
516 });
517
518 setGeneratedKey(data.publicKeyDid);
519 setKeyJson(data);
520 updateStepStatus(0, "completed");
521 } catch (error) {
522 console.error("Key generation failed:", error);
523 updateStepStatus(
524 0,
525 "error",
526 error instanceof Error ? error.message : String(error),
527 );
528 }
529 };
530
531 const getStepIcon = (status: PlcUpdateStep["status"]) => {
532 switch (status) {
533 case "pending":
534 return (
535 <div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
536 <div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
537 </div>
538 );
539 case "in-progress":
540 return (
541 <div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center">
542 <div class="w-3 h-3 rounded-full bg-blue-500" />
543 </div>
544 );
545 case "verifying":
546 return (
547 <div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center">
548 <div class="w-3 h-3 rounded-full bg-yellow-500" />
549 </div>
550 );
551 case "completed":
552 return (
553 <div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
554 <svg
555 class="w-5 h-5 text-white"
556 fill="none"
557 stroke="currentColor"
558 viewBox="0 0 24 24"
559 >
560 <path
561 stroke-linecap="round"
562 stroke-linejoin="round"
563 stroke-width="2"
564 d="M5 13l4 4L19 7"
565 />
566 </svg>
567 </div>
568 );
569 case "error":
570 return (
571 <div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
572 <svg
573 class="w-5 h-5 text-white"
574 fill="none"
575 stroke="currentColor"
576 viewBox="0 0 24 24"
577 >
578 <path
579 stroke-linecap="round"
580 stroke-linejoin="round"
581 stroke-width="2"
582 d="M6 18L18 6M6 6l12 12"
583 />
584 </svg>
585 </div>
586 );
587 }
588 };
589
590 const getStepClasses = (status: PlcUpdateStep["status"]) => {
591 const baseClasses =
592 "flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
593 switch (status) {
594 case "pending":
595 return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
596 case "in-progress":
597 return `${baseClasses} bg-blue-50 dark:bg-blue-900`;
598 case "verifying":
599 return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`;
600 case "completed":
601 return `${baseClasses} bg-green-50 dark:bg-green-900`;
602 case "error":
603 return `${baseClasses} bg-red-50 dark:bg-red-900`;
604 }
605 };
606
607 const requestNewToken = async () => {
608 try {
609 console.log("Requesting new token...");
610 const res = await fetch("/api/plc/token", {
611 method: "GET",
612 });
613 const text = await res.text();
614 console.log("Token request response:", text);
615
616 if (!res.ok) {
617 throw new Error(text || "Failed to request new token");
618 }
619
620 let data;
621 try {
622 data = JSON.parse(text);
623 if (!data.success) {
624 throw new Error(data.message || "Failed to request token");
625 }
626 } catch {
627 throw new Error("Invalid response from server");
628 }
629
630 // Clear any existing error and token
631 setEmailToken("");
632 updateStepStatus(1, "in-progress");
633 updateStepStatus(2, "pending");
634 } catch (error) {
635 console.error("Failed to request new token:", error);
636 updateStepStatus(
637 1,
638 "error",
639 error instanceof Error ? error.message : String(error),
640 );
641 }
642 };
643
644 if (!hasStarted) {
645 return (
646 <div class="space-y-6">
647 <div class="ticket bg-white dark:bg-slate-800 p-6 relative">
648 <div class="boarding-label text-amber-500 dark:text-amber-400 font-mono font-bold tracking-wider text-sm mb-2">
649 {contentChunks[currentChunkIndex].subtitle}
650 </div>
651
652 <div class="flex justify-between items-start mb-4">
653 <h3 class="text-2xl font-mono text-slate-800 dark:text-slate-200">
654 {contentChunks[currentChunkIndex].title}
655 </h3>
656 </div>
657
658 {/* Main Description */}
659 <div class="mb-6">{contentChunks[currentChunkIndex].content}</div>
660
661 {/* Navigation */}
662 <div class="mt-8 border-t border-dashed border-slate-200 dark:border-slate-700 pt-4">
663 <div class="flex justify-between items-center">
664 <button
665 type="button"
666 onClick={() =>
667 setCurrentChunkIndex((prev) => Math.max(0, prev - 1))}
668 class={`px-4 py-2 font-mono text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors duration-200 flex items-center space-x-2 ${
669 currentChunkIndex === 0 ? "invisible" : ""
670 }`}
671 >
672 <svg
673 class="w-5 h-5 rotate-180"
674 fill="none"
675 stroke="currentColor"
676 viewBox="0 0 24 24"
677 >
678 <path
679 stroke-linecap="round"
680 stroke-linejoin="round"
681 stroke-width="2"
682 d="M9 5l7 7-7 7"
683 />
684 </svg>
685 <span>Previous Gate</span>
686 </button>
687
688 {currentChunkIndex === contentChunks.length - 1
689 ? (
690 <button
691 type="button"
692 onClick={handleStart}
693 class="px-6 py-2 bg-amber-500 hover:bg-amber-600 text-white font-mono rounded-md transition-colors duration-200 flex items-center space-x-2"
694 >
695 <span>Begin Key Generation</span>
696 <svg
697 class="w-5 h-5"
698 fill="none"
699 stroke="currentColor"
700 viewBox="0 0 24 24"
701 >
702 <path
703 stroke-linecap="round"
704 stroke-linejoin="round"
705 stroke-width="2"
706 d="M9 5l7 7-7 7"
707 />
708 </svg>
709 </button>
710 )
711 : (
712 <button
713 type="button"
714 onClick={() =>
715 setCurrentChunkIndex((prev) =>
716 Math.min(contentChunks.length - 1, prev + 1)
717 )}
718 class="px-4 py-2 font-mono text-slate-600 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200 transition-colors duration-200 flex items-center space-x-2"
719 >
720 <span>Next Gate</span>
721 <svg
722 class="w-5 h-5"
723 fill="none"
724 stroke="currentColor"
725 viewBox="0 0 24 24"
726 >
727 <path
728 stroke-linecap="round"
729 stroke-linejoin="round"
730 stroke-width="2"
731 d="M9 5l7 7-7 7"
732 />
733 </svg>
734 </button>
735 )}
736 </div>
737
738 {/* Progress Dots */}
739 <div class="flex justify-center space-x-3 mt-4">
740 {contentChunks.map((_, index) => (
741 <div
742 key={index}
743 class={`h-1.5 w-8 rounded-full transition-colors duration-200 ${
744 index === currentChunkIndex
745 ? "bg-amber-500"
746 : "bg-slate-200 dark:bg-slate-700"
747 }`}
748 />
749 ))}
750 </div>
751 </div>
752 </div>
753 </div>
754 );
755 }
756
757 return (
758 <div class="space-y-8">
759 {/* Progress Steps */}
760 <div class="space-y-4">
761 <div class="flex items-center justify-between">
762 <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
763 Key Generation Progress
764 </h3>
765 {/* Add a help tooltip */}
766 <div class="relative group">
767 <button class="text-gray-400 hover:text-gray-500" type="button">
768 <svg
769 class="w-5 h-5"
770 fill="none"
771 stroke="currentColor"
772 viewBox="0 0 24 24"
773 >
774 <path
775 stroke-linecap="round"
776 stroke-linejoin="round"
777 stroke-width="2"
778 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
779 />
780 </svg>
781 </button>
782 <div class="absolute right-0 w-64 p-2 mt-2 space-y-1 text-sm bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 hidden group-hover:block z-10">
783 <p class="text-gray-600 dark:text-gray-400">
784 Follow these steps to securely add a new rotation key to your
785 PLC record. Each step requires completion before proceeding.
786 </p>
787 </div>
788 </div>
789 </div>
790
791 {/* Steps with enhanced visual hierarchy */}
792 {steps.map((step, index) => (
793 <div
794 key={step.name}
795 class={`${getStepClasses(step.status)} ${
796 step.status === "in-progress"
797 ? "ring-2 ring-blue-500 ring-opacity-50"
798 : ""
799 }`}
800 >
801 <div class="flex-shrink-0">{getStepIcon(step.status)}</div>
802 <div class="flex-1 min-w-0">
803 <div class="flex items-center justify-between">
804 <p
805 class={`font-medium ${
806 step.status === "error"
807 ? "text-red-900 dark:text-red-200"
808 : step.status === "completed"
809 ? "text-green-900 dark:text-green-200"
810 : step.status === "in-progress"
811 ? "text-blue-900 dark:text-blue-200"
812 : "text-gray-900 dark:text-gray-200"
813 }`}
814 >
815 {getStepDisplayName(step, index)}
816 </p>
817 {/* Add step number */}
818 <span class="text-sm text-gray-500 dark:text-gray-400">
819 Step {index + 1} of {steps.length}
820 </span>
821 </div>
822
823 {step.error && (
824 <div class="mt-2 p-2 bg-red-50 dark:bg-red-900/50 rounded-md">
825 <p class="text-sm text-red-600 dark:text-red-400 flex items-center">
826 <svg
827 class="w-4 h-4 mr-1"
828 fill="none"
829 stroke="currentColor"
830 viewBox="0 0 24 24"
831 >
832 <path
833 stroke-linecap="round"
834 stroke-linejoin="round"
835 stroke-width="2"
836 d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
837 />
838 </svg>
839 {(() => {
840 try {
841 const err = JSON.parse(step.error);
842 return err.message || step.error;
843 } catch {
844 return step.error;
845 }
846 })()}
847 </p>
848 </div>
849 )}
850
851 {/* Key Download Warning */}
852 {index === 0 &&
853 step.status === "completed" &&
854 !hasContinuedPastDownload && (
855 <div class="mt-4 space-y-4">
856 <div class="bg-yellow-50 dark:bg-yellow-900/50 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800">
857 <div class="flex items-start">
858 <div class="flex-shrink-0">
859 <svg
860 class="h-5 w-5 text-yellow-400"
861 viewBox="0 0 20 20"
862 fill="currentColor"
863 >
864 <path
865 fill-rule="evenodd"
866 d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
867 clip-rule="evenodd"
868 />
869 </svg>
870 </div>
871 <div class="ml-3">
872 <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
873 Critical Security Step
874 </h3>
875 <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
876 <p class="mb-2">
877 Your rotation key grants control over your identity:
878 </p>
879 <ul class="list-disc pl-5 space-y-2">
880 <li>
881 <strong>Store Securely:</strong>{" "}
882 Use a password manager
883 </li>
884 <li>
885 <strong>Keep Private:</strong>{" "}
886 Never share with anyone
887 </li>
888 <li>
889 <strong>Backup:</strong> Keep a secure backup copy
890 </li>
891 <li>
892 <strong>Required:</strong>{" "}
893 Needed for future DID modifications
894 </li>
895 </ul>
896 </div>
897 </div>
898 </div>
899 </div>
900
901 <div class="flex items-center justify-between">
902 <div class="flex items-center space-x-3">
903 <button
904 type="button"
905 onClick={handleDownload}
906 class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
907 >
908 <svg
909 class="w-5 h-5"
910 fill="none"
911 stroke="currentColor"
912 viewBox="0 0 24 24"
913 >
914 <path
915 stroke-linecap="round"
916 stroke-linejoin="round"
917 stroke-width="2"
918 d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
919 />
920 </svg>
921 <span>Download Key</span>
922 </button>
923
924 {hasDownloadedKey && (
925 <button
926 type="button"
927 onClick={() => {
928 console.log(
929 "Continue clicked, proceeding to PLC update",
930 );
931 setHasContinuedPastDownload(true);
932 handleStartPlcUpdate(keyJson.publicKeyDid);
933 }}
934 class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
935 >
936 <svg
937 class="w-5 h-5"
938 fill="none"
939 stroke="currentColor"
940 viewBox="0 0 24 24"
941 >
942 <path
943 stroke-linecap="round"
944 stroke-linejoin="round"
945 stroke-width="2"
946 d="M9 5l7 7-7 7"
947 />
948 </svg>
949 <span>Continue</span>
950 </button>
951 )}
952 </div>
953
954 {!hasDownloadedKey && (
955 <div class="flex items-center text-sm text-red-600 dark:text-red-400">
956 <svg
957 class="w-4 h-4 mr-1"
958 fill="none"
959 stroke="currentColor"
960 viewBox="0 0 24 24"
961 >
962 <path
963 stroke-linecap="round"
964 stroke-linejoin="round"
965 stroke-width="2"
966 d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
967 />
968 </svg>
969 Download required to proceed
970 </div>
971 )}
972 </div>
973 </div>
974 )}
975
976 {/* Email Code Input */}
977 {index === 1 &&
978 (step.status === "in-progress" ||
979 step.status === "verifying") &&
980 step.name ===
981 "Enter the code sent to your email to complete PLC update" &&
982 (
983 <div class="mt-4 space-y-4">
984 <div class="bg-blue-50 dark:bg-blue-900/50 p-4 rounded-lg">
985 <p class="text-sm text-blue-800 dark:text-blue-200 mb-3">
986 Check your email for the verification code to complete
987 the PLC update:
988 </p>
989 <div class="flex space-x-2">
990 <div class="flex-1 relative">
991 <input
992 type="text"
993 value={emailToken}
994 onChange={(e) =>
995 setEmailToken(e.currentTarget.value)}
996 placeholder="Enter verification code"
997 class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-blue-400 dark:focus:ring-blue-400"
998 />
999 </div>
1000 <button
1001 type="button"
1002 onClick={handleTokenSubmit}
1003 disabled={!emailToken || step.status === "verifying"}
1004 class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
1005 >
1006 <span>
1007 {step.status === "verifying"
1008 ? "Verifying..."
1009 : "Verify"}
1010 </span>
1011 <svg
1012 class="w-4 h-4"
1013 fill="none"
1014 stroke="currentColor"
1015 viewBox="0 0 24 24"
1016 >
1017 <path
1018 stroke-linecap="round"
1019 stroke-linejoin="round"
1020 stroke-width="2"
1021 d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
1022 />
1023 </svg>
1024 </button>
1025 </div>
1026 {step.error && (
1027 <div class="mt-2 p-2 bg-red-50 dark:bg-red-900/50 rounded-md">
1028 <p class="text-sm text-red-600 dark:text-red-400 flex items-center">
1029 <svg
1030 class="w-4 h-4 mr-1"
1031 fill="none"
1032 stroke="currentColor"
1033 viewBox="0 0 24 24"
1034 >
1035 <path
1036 stroke-linecap="round"
1037 stroke-linejoin="round"
1038 stroke-width="2"
1039 d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
1040 />
1041 </svg>
1042 {step.error}
1043 </p>
1044 {step.error
1045 .toLowerCase()
1046 .includes("token is invalid") && (
1047 <div class="mt-2">
1048 <p class="text-sm text-red-500 dark:text-red-300 mb-2">
1049 The verification code may have expired. Request
1050 a new code to try again.
1051 </p>
1052 <button
1053 type="button"
1054 onClick={requestNewToken}
1055 class="text-sm px-3 py-1 bg-red-100 hover:bg-red-200 dark:bg-red-800 dark:hover:bg-red-700 text-red-700 dark:text-red-200 rounded-md transition-colors duration-200 flex items-center space-x-1"
1056 >
1057 <svg
1058 class="w-4 h-4"
1059 fill="none"
1060 stroke="currentColor"
1061 viewBox="0 0 24 24"
1062 >
1063 <path
1064 stroke-linecap="round"
1065 stroke-linejoin="round"
1066 stroke-width="2"
1067 d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
1068 />
1069 </svg>
1070 <span>Request New Code</span>
1071 </button>
1072 </div>
1073 )}
1074 </div>
1075 )}
1076 </div>
1077 </div>
1078 )}
1079 </div>
1080 </div>
1081 ))}
1082 </div>
1083
1084 {/* Success Message */}
1085 {steps[2].status === "completed" && (
1086 <div class="p-4 bg-green-50 dark:bg-green-900/50 rounded-lg border-2 border-green-200 dark:border-green-800">
1087 <div class="flex items-center space-x-3 mb-4">
1088 <svg
1089 class="w-6 h-6 text-green-500"
1090 fill="none"
1091 stroke="currentColor"
1092 viewBox="0 0 24 24"
1093 >
1094 <path
1095 stroke-linecap="round"
1096 stroke-linejoin="round"
1097 stroke-width="2"
1098 d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
1099 />
1100 </svg>
1101 <h4 class="text-lg font-medium text-green-800 dark:text-green-200">
1102 PLC Update Successful!
1103 </h4>
1104 </div>
1105 <p class="text-sm text-green-700 dark:text-green-300 mb-4">
1106 Your rotation key has been successfully added to your PLC record.
1107 You can now use this key for future DID modifications.
1108 </p>
1109 <div class="flex space-x-4">
1110 <button
1111 type="button"
1112 onClick={async () => {
1113 try {
1114 const response = await fetch("/api/logout", {
1115 method: "POST",
1116 credentials: "include",
1117 });
1118 if (!response.ok) {
1119 throw new Error("Logout failed");
1120 }
1121 globalThis.location.href = "/";
1122 } catch (error) {
1123 console.error("Failed to logout:", error);
1124 }
1125 }}
1126 class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
1127 >
1128 <svg
1129 class="w-5 h-5"
1130 fill="none"
1131 stroke="currentColor"
1132 viewBox="0 0 24 24"
1133 >
1134 <path
1135 stroke-linecap="round"
1136 stroke-linejoin="round"
1137 stroke-width="2"
1138 d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
1139 />
1140 </svg>
1141 <span>Sign Out</span>
1142 </button>
1143 <a
1144 href="https://ko-fi.com/knotbin"
1145 target="_blank"
1146 class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200 flex items-center space-x-2"
1147 >
1148 <svg
1149 class="w-5 h-5"
1150 fill="none"
1151 stroke="currentColor"
1152 viewBox="0 0 24 24"
1153 >
1154 <path
1155 stroke-linecap="round"
1156 stroke-linejoin="round"
1157 stroke-width="2"
1158 d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
1159 />
1160 </svg>
1161 <span>Support Us</span>
1162 </a>
1163 </div>
1164 </div>
1165 )}
1166 </div>
1167 );
1168}