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 AccountCreationStepProps extends StepCommonProps {
10 isActive: boolean;
11}
12
13export default function AccountCreationStep({
14 credentials,
15 onStepComplete,
16 onStepError,
17 isActive,
18}: AccountCreationStepProps) {
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 startAccountCreation();
29 }
30 }, [isActive]);
31
32 const startAccountCreation = async () => {
33 setStatus("in-progress");
34 setError(undefined);
35
36 try {
37 const createRes = await fetch("/api/migrate/create", {
38 method: "POST",
39 headers: { "Content-Type": "application/json" },
40 body: JSON.stringify({
41 service: credentials.service,
42 handle: credentials.handle,
43 password: credentials.password,
44 email: credentials.email,
45 ...(credentials.invite ? { invite: credentials.invite } : {}),
46 }),
47 });
48
49 const responseText = await createRes.text();
50
51 if (!createRes.ok) {
52 const parsed = parseApiResponse(responseText);
53 throw new Error(parsed.message || "Failed to create account");
54 }
55
56 const parsed = parseApiResponse(responseText);
57 if (!parsed.success) {
58 throw new Error(parsed.message || "Account creation failed");
59 }
60
61 // Verify the account creation
62 await verifyAccountCreation();
63 } catch (error) {
64 const errorMessage = error instanceof Error
65 ? error.message
66 : String(error);
67 setError(errorMessage);
68 setStatus("error");
69 onStepError(errorMessage);
70 }
71 };
72
73 const verifyAccountCreation = async () => {
74 setStatus("verifying");
75
76 try {
77 const result = await verifyMigrationStep(1);
78
79 if (result.ready) {
80 setStatus("completed");
81 setRetryCount(0);
82 setShowContinueAnyway(false);
83 onStepComplete();
84 } else {
85 const statusDetails = {
86 activated: result.activated,
87 validDid: result.validDid,
88 };
89 const errorMessage = `${
90 result.reason || "Verification failed"
91 }\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
92
93 setRetryCount((prev) => prev + 1);
94 if (retryCount >= 1) {
95 setShowContinueAnyway(true);
96 }
97
98 setError(errorMessage);
99 setStatus("error");
100 onStepError(errorMessage, true);
101 }
102 } catch (error) {
103 const errorMessage = error instanceof Error
104 ? error.message
105 : String(error);
106 setRetryCount((prev) => prev + 1);
107 if (retryCount >= 1) {
108 setShowContinueAnyway(true);
109 }
110
111 setError(errorMessage);
112 setStatus("error");
113 onStepError(errorMessage, true);
114 }
115 };
116
117 const retryVerification = async () => {
118 await verifyAccountCreation();
119 };
120
121 const continueAnyway = () => {
122 setStatus("completed");
123 setShowContinueAnyway(false);
124 onStepComplete();
125 };
126
127 return (
128 <MigrationStep
129 name="Create Account"
130 status={status}
131 error={error}
132 isVerificationError={status === "error" &&
133 error?.includes("Verification failed")}
134 index={0}
135 onRetryVerification={retryVerification}
136 >
137 {status === "error" && showContinueAnyway && (
138 <div class="flex space-x-2 mt-2">
139 <button
140 type="button"
141 onClick={continueAnyway}
142 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
143 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
144 >
145 Continue Anyway
146 </button>
147 </div>
148 )}
149 </MigrationStep>
150 );
151}