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}