Graphical PDS migrator for AT Protocol
1import { useEffect, useState } from "preact/hooks";
2import { MigrationStep } from "../../components/MigrationStep.tsx";
3import {
4 parseApiResponse,
5 StepCommonProps,
6 verifyMigrationStep,
7} from "../../lib/migration-types.ts";
8
9interface FinalizationStepProps extends StepCommonProps {
10 isActive: boolean;
11}
12
13export default function FinalizationStep({
14 credentials: _credentials,
15 onStepComplete,
16 onStepError,
17 isActive,
18}: FinalizationStepProps) {
19 const [status, setStatus] = useState<
20 "pending" | "in-progress" | "verifying" | "completed" | "error"
21 >("pending");
22 const [error, setError] = useState<string>();
23 const [retryCount, setRetryCount] = useState(0);
24 const [showContinueAnyway, setShowContinueAnyway] = useState(false);
25
26 useEffect(() => {
27 if (isActive && status === "pending") {
28 startFinalization();
29 }
30 }, [isActive]);
31
32 const startFinalization = async () => {
33 setStatus("in-progress");
34 setError(undefined);
35
36 try {
37 const finalizeRes = await fetch("/api/migrate/finalize", {
38 method: "POST",
39 headers: { "Content-Type": "application/json" },
40 });
41
42 const finalizeData = await finalizeRes.text();
43 if (!finalizeRes.ok) {
44 const parsed = parseApiResponse(finalizeData);
45 throw new Error(parsed.message || "Failed to finalize migration");
46 }
47
48 const parsed = parseApiResponse(finalizeData);
49 if (!parsed.success) {
50 throw new Error(parsed.message || "Finalization failed");
51 }
52
53 // Verify the finalization
54 await verifyFinalization();
55 } catch (error) {
56 const errorMessage = error instanceof Error
57 ? error.message
58 : String(error);
59 setError(errorMessage);
60 setStatus("error");
61 onStepError(errorMessage);
62 }
63 };
64
65 const verifyFinalization = async () => {
66 setStatus("verifying");
67
68 try {
69 const result = await verifyMigrationStep(4);
70
71 if (result.ready) {
72 setStatus("completed");
73 setRetryCount(0);
74 setShowContinueAnyway(false);
75 onStepComplete();
76 } else {
77 const statusDetails = {
78 activated: result.activated,
79 validDid: result.validDid,
80 };
81 const errorMessage = `${
82 result.reason || "Verification failed"
83 }\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
84
85 setRetryCount((prev) => prev + 1);
86 if (retryCount >= 1) {
87 setShowContinueAnyway(true);
88 }
89
90 setError(errorMessage);
91 setStatus("error");
92 onStepError(errorMessage, true);
93 }
94 } catch (error) {
95 const errorMessage = error instanceof Error
96 ? error.message
97 : String(error);
98 setRetryCount((prev) => prev + 1);
99 if (retryCount >= 1) {
100 setShowContinueAnyway(true);
101 }
102
103 setError(errorMessage);
104 setStatus("error");
105 onStepError(errorMessage, true);
106 }
107 };
108
109 const retryVerification = async () => {
110 await verifyFinalization();
111 };
112
113 const continueAnyway = () => {
114 setStatus("completed");
115 setShowContinueAnyway(false);
116 onStepComplete();
117 };
118
119 return (
120 <MigrationStep
121 name="Finalize Migration"
122 status={status}
123 error={error}
124 isVerificationError={status === "error" &&
125 error?.includes("Verification failed")}
126 index={3}
127 onRetryVerification={retryVerification}
128 >
129 {status === "error" && showContinueAnyway && (
130 <div class="flex space-x-2 mt-2">
131 <button
132 type="button"
133 onClick={continueAnyway}
134 class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 hover:bg-gray-100 rounded transition-colors duration-200
135 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
136 >
137 Continue Anyway
138 </button>
139 </div>
140 )}
141 </MigrationStep>
142 );
143}