···
import { useEffect, useState } from "preact/hooks";
4
-
* The migration state info.
5
-
* @type {MigrationStateInfo}
7
-
interface MigrationStateInfo {
8
-
state: "up" | "issue" | "maintenance";
10
-
allowMigration: boolean;
2
+
import { MigrationStateInfo } from "../lib/migration-types.ts";
3
+
import AccountCreationStep from "./migration-steps/AccountCreationStep.tsx";
4
+
import DataMigrationStep from "./migration-steps/DataMigrationStep.tsx";
5
+
import IdentityMigrationStep from "./migration-steps/IdentityMigrationStep.tsx";
6
+
import FinalizationStep from "./migration-steps/FinalizationStep.tsx";
7
+
import MigrationCompletion from "../components/MigrationCompletion.tsx";
* The migration progress props.
···
26
-
* The migration step.
27
-
* @type {MigrationStep}
29
-
interface MigrationStep {
31
-
status: "pending" | "in-progress" | "verifying" | "completed" | "error";
33
-
isVerificationError?: boolean;
* The migration progress component.
* @param props - The migration progress props
···
export default function MigrationProgress(props: MigrationProgressProps) {
43
-
const [token, setToken] = useState("");
const [migrationState, setMigrationState] = useState<
MigrationStateInfo | null
47
-
const [retryAttempts, setRetryAttempts] = useState<Record<number, number>>(
50
-
const [showContinueAnyway, setShowContinueAnyway] = useState<
51
-
Record<number, boolean>
54
-
const [steps, setSteps] = useState<MigrationStep[]>([
55
-
{ name: "Create Account", status: "pending" },
56
-
{ name: "Migrate Data", status: "pending" },
57
-
{ name: "Migrate Identity", status: "pending" },
58
-
{ name: "Finalize Migration", status: "pending" },
61
-
const updateStepStatus = (
63
-
status: MigrationStep["status"],
65
-
isVerificationError?: boolean,
68
-
`Updating step ${index} to ${status}${
69
-
error ? ` with error: ${error}` : ""
72
-
setSteps((prevSteps) =>
73
-
prevSteps.map((step, i) =>
75
-
? { ...step, status, error, isVerificationError }
81
-
isVerificationError: undefined,
31
+
const [currentStep, setCurrentStep] = useState(0);
32
+
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
33
+
const [hasError, setHasError] = useState(false);
35
+
const credentials = {
36
+
service: props.service,
37
+
handle: props.handle,
39
+
password: props.password,
40
+
invite: props.invite,
const validateParams = () => {
if (!props.service?.trim()) {
90
-
updateStepStatus(0, "error", "Missing service URL");
if (!props.handle?.trim()) {
94
-
updateStepStatus(0, "error", "Missing handle");
if (!props.email?.trim()) {
98
-
updateStepStatus(0, "error", "Missing email");
if (!props.password?.trim()) {
102
-
updateStepStatus(0, "error", "Missing password");
···
setMigrationState(migrationData);
if (!migrationData.allowMigration) {
126
-
updateStepStatus(0, "error", migrationData.message);
console.error("Failed to check migration state:", error);
132
-
updateStepStatus(0, "error", "Unable to verify migration availability");
···
141
-
startMigration().catch((error) => {
142
-
console.error("Unhandled migration error:", error);
146
-
error.message || "Unknown error occurred",
96
+
// Start with the first step
154
-
const getStepDisplayName = (step: MigrationStep, index: number) => {
155
-
if (step.status === "completed") {
158
-
return "Account Created";
160
-
return "Data Migrated";
162
-
return "Identity Migrated";
164
-
return "Migration Finalized";
168
-
if (step.status === "in-progress") {
171
-
return "Creating your new account...";
173
-
return "Migrating your data...";
175
-
return step.name ===
176
-
"Enter the token sent to your email to complete identity migration"
178
-
: "Migrating your identity...";
180
-
return "Finalizing migration...";
184
-
if (step.status === "verifying") {
187
-
return "Verifying account creation...";
189
-
return "Verifying data migration...";
191
-
return "Verifying identity migration...";
193
-
return "Verifying migration completion...";
200
-
const startMigration = async () => {
202
-
// Step 1: Create Account
203
-
updateStepStatus(0, "in-progress");
204
-
console.log("Starting account creation...");
207
-
const createRes = await fetch("/api/migrate/create", {
209
-
headers: { "Content-Type": "application/json" },
210
-
body: JSON.stringify({
211
-
service: props.service,
212
-
handle: props.handle,
213
-
password: props.password,
214
-
email: props.email,
215
-
...(props.invite ? { invite: props.invite } : {}),
219
-
console.log("Create account response status:", createRes.status);
220
-
const responseText = await createRes.text();
221
-
console.log("Create account response:", responseText);
223
-
if (!createRes.ok) {
225
-
const json = JSON.parse(responseText);
226
-
throw new Error(json.message || "Failed to create account");
228
-
throw new Error(responseText || "Failed to create account");
233
-
const jsonData = JSON.parse(responseText);
234
-
if (!jsonData.success) {
235
-
throw new Error(jsonData.message || "Account creation failed");
238
-
console.log("Response is not JSON or lacks success field:", e);
241
-
updateStepStatus(0, "verifying");
242
-
const verified = await verifyStep(0);
245
-
"Account creation: Verification failed, waiting for user action",
250
-
// If verification succeeds, continue to data migration
251
-
await startDataMigration();
256
-
error instanceof Error ? error.message : String(error),
261
-
console.error("Migration error in try/catch:", error);
265
-
const handleIdentityMigration = async () => {
266
-
if (!token) return;
269
-
const identityRes = await fetch(
270
-
`/api/migrate/identity/sign?token=${encodeURIComponent(token)}`,
273
-
headers: { "Content-Type": "application/json" },
277
-
const identityData = await identityRes.text();
278
-
if (!identityRes.ok) {
280
-
const json = JSON.parse(identityData);
282
-
json.message || "Failed to complete identity migration",
286
-
identityData || "Failed to complete identity migration",
293
-
data = JSON.parse(identityData);
294
-
if (!data.success) {
295
-
throw new Error(data.message || "Identity migration failed");
298
-
throw new Error("Invalid response from server");
301
-
updateStepStatus(2, "verifying");
302
-
const verified = await verifyStep(2);
305
-
"Identity migration: Verification failed, waiting for user action",
310
-
// If verification succeeds, continue to finalization
311
-
await startFinalization();
313
-
console.error("Identity migration error:", error);
317
-
error instanceof Error ? error.message : String(error),
322
-
const getStepIcon = (status: MigrationStep["status"]) => {
326
-
<div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
327
-
<div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
330
-
case "in-progress":
332
-
<div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center">
333
-
<div class="w-3 h-3 rounded-full bg-blue-500" />
338
-
<div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center">
339
-
<div class="w-3 h-3 rounded-full bg-yellow-500" />
344
-
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
346
-
class="w-5 h-5 text-white"
348
-
stroke="currentColor"
349
-
viewBox="0 0 24 24"
352
-
stroke-linecap="round"
353
-
stroke-linejoin="round"
362
-
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
364
-
class="w-5 h-5 text-white"
366
-
stroke="currentColor"
367
-
viewBox="0 0 24 24"
370
-
stroke-linecap="round"
371
-
stroke-linejoin="round"
373
-
d="M6 18L18 6M6 6l12 12"
381
-
const getStepClasses = (status: MigrationStep["status"]) => {
382
-
const baseClasses =
383
-
"flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
386
-
return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
387
-
case "in-progress":
388
-
return `${baseClasses} bg-blue-50 dark:bg-blue-900`;
390
-
return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`;
392
-
return `${baseClasses} bg-green-50 dark:bg-green-900`;
394
-
return `${baseClasses} bg-red-50 dark:bg-red-900`;
398
-
// Helper to verify a step after completion
399
-
const verifyStep = async (stepNum: number) => {
400
-
console.log(`Verification: Starting step ${stepNum + 1}`);
401
-
updateStepStatus(stepNum, "verifying");
403
-
console.log(`Verification: Fetching status for step ${stepNum + 1}`);
404
-
const res = await fetch(`/api/migrate/status?step=${stepNum + 1}`);
405
-
console.log(`Verification: Status response status:`, res.status);
406
-
const data = await res.json();
407
-
console.log(`Verification: Status data for step ${stepNum + 1}:`, data);
410
-
console.log(`Verification: Step ${stepNum + 1} is ready`);
411
-
updateStepStatus(stepNum, "completed");
412
-
// Reset retry state on success
413
-
setRetryAttempts((prev) => ({ ...prev, [stepNum]: 0 }));
414
-
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: false }));
103
+
const handleStepComplete = (stepIndex: number) => {
104
+
console.log(`Step ${stepIndex} completed`);
105
+
setCompletedSteps((prev) => new Set([...prev, stepIndex]));
416
-
// Continue to next step if not the last one
418
-
setTimeout(() => continueToNextStep(stepNum + 1), 500);
424
-
`Verification: Step ${stepNum + 1} is not ready:`,
427
-
const statusDetails = {
428
-
activated: data.activated,
429
-
validDid: data.validDid,
430
-
repoCommit: data.repoCommit,
431
-
repoRev: data.repoRev,
432
-
repoBlocks: data.repoBlocks,
433
-
expectedRecords: data.expectedRecords,
434
-
indexedRecords: data.indexedRecords,
435
-
privateStateValues: data.privateStateValues,
436
-
expectedBlobs: data.expectedBlobs,
437
-
importedBlobs: data.importedBlobs,
440
-
`Verification: Step ${stepNum + 1} status details:`,
443
-
const errorMessage = `${
444
-
data.reason || "Verification failed"
445
-
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
447
-
// Track retry attempts
448
-
const currentAttempts = retryAttempts[stepNum] || 0;
449
-
setRetryAttempts((prev) => ({
451
-
[stepNum]: currentAttempts + 1,
454
-
// Show continue anyway option if this is the second failure
455
-
if (currentAttempts >= 1) {
456
-
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: true }));
459
-
updateStepStatus(stepNum, "error", errorMessage, true);
463
-
console.error(`Verification: Error in step ${stepNum + 1}:`, e);
464
-
const currentAttempts = retryAttempts[stepNum] || 0;
465
-
setRetryAttempts((prev) => ({ ...prev, [stepNum]: currentAttempts + 1 }));
467
-
// Show continue anyway option if this is the second failure
468
-
if (currentAttempts >= 1) {
469
-
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: true }));
475
-
e instanceof Error ? e.message : String(e),
107
+
// Move to next step if not the last one
108
+
if (stepIndex < 3) {
109
+
setCurrentStep(stepIndex + 1);
482
-
const retryVerification = async (stepNum: number) => {
483
-
console.log(`Retrying verification for step ${stepNum + 1}`);
484
-
await verifyStep(stepNum);
113
+
const handleStepError = (
116
+
isVerificationError?: boolean,
118
+
console.error(`Step ${stepIndex} error:`, error, { isVerificationError });
119
+
// Errors are handled within each step component
487
-
const continueAnyway = (stepNum: number) => {
488
-
console.log(`Continuing anyway for step ${stepNum + 1}`);
489
-
updateStepStatus(stepNum, "completed");
490
-
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: false }));
492
-
// Continue with next step if not the last one
494
-
continueToNextStep(stepNum + 1);
122
+
const isStepActive = (stepIndex: number) => {
123
+
return currentStep === stepIndex && !hasError;
498
-
const continueToNextStep = async (stepNum: number) => {
501
-
// Continue to data migration
502
-
await startDataMigration();
505
-
// Continue to identity migration
506
-
await startIdentityMigration();
509
-
// Continue to finalization
510
-
await startFinalization();
126
+
const _isStepCompleted = (stepIndex: number) => {
127
+
return completedSteps.has(stepIndex);
515
-
const startDataMigration = async () => {
516
-
// Step 2: Migrate Data
517
-
updateStepStatus(1, "in-progress");
518
-
console.log("Starting data migration...");
521
-
// Step 2.1: Migrate Repo
522
-
console.log("Data migration: Starting repo migration");
523
-
const repoRes = await fetch("/api/migrate/data/repo", {
525
-
headers: { "Content-Type": "application/json" },
528
-
console.log("Repo migration: Response status:", repoRes.status);
529
-
const repoText = await repoRes.text();
530
-
console.log("Repo migration: Raw response:", repoText);
534
-
const json = JSON.parse(repoText);
535
-
console.error("Repo migration: Error response:", json);
536
-
throw new Error(json.message || "Failed to migrate repo");
538
-
console.error("Repo migration: Non-JSON error response:", repoText);
539
-
throw new Error(repoText || "Failed to migrate repo");
543
-
// Step 2.2: Migrate Blobs
544
-
console.log("Data migration: Starting blob migration");
545
-
const blobsRes = await fetch("/api/migrate/data/blobs", {
547
-
headers: { "Content-Type": "application/json" },
550
-
console.log("Blob migration: Response status:", blobsRes.status);
551
-
const blobsText = await blobsRes.text();
552
-
console.log("Blob migration: Raw response:", blobsText);
554
-
if (!blobsRes.ok) {
556
-
const json = JSON.parse(blobsText);
557
-
console.error("Blob migration: Error response:", json);
558
-
throw new Error(json.message || "Failed to migrate blobs");
561
-
"Blob migration: Non-JSON error response:",
564
-
throw new Error(blobsText || "Failed to migrate blobs");
568
-
// Step 2.3: Migrate Preferences
569
-
console.log("Data migration: Starting preferences migration");
570
-
const prefsRes = await fetch("/api/migrate/data/prefs", {
572
-
headers: { "Content-Type": "application/json" },
575
-
console.log("Preferences migration: Response status:", prefsRes.status);
576
-
const prefsText = await prefsRes.text();
577
-
console.log("Preferences migration: Raw response:", prefsText);
579
-
if (!prefsRes.ok) {
581
-
const json = JSON.parse(prefsText);
582
-
console.error("Preferences migration: Error response:", json);
583
-
throw new Error(json.message || "Failed to migrate preferences");
586
-
"Preferences migration: Non-JSON error response:",
589
-
throw new Error(prefsText || "Failed to migrate preferences");
593
-
console.log("Data migration: Starting verification");
594
-
updateStepStatus(1, "verifying");
595
-
const verified = await verifyStep(1);
596
-
console.log("Data migration: Verification result:", verified);
599
-
"Data migration: Verification failed, waiting for user action",
604
-
// If verification succeeds, continue to next step
605
-
await startIdentityMigration();
607
-
console.error("Data migration: Error caught:", error);
611
-
error instanceof Error ? error.message : String(error),
617
-
const startIdentityMigration = async () => {
618
-
// Step 3: Request Identity Migration
619
-
updateStepStatus(2, "in-progress");
620
-
console.log("Requesting identity migration...");
623
-
const requestRes = await fetch("/api/migrate/identity/request", {
625
-
headers: { "Content-Type": "application/json" },
628
-
console.log("Identity request response status:", requestRes.status);
629
-
const requestText = await requestRes.text();
630
-
console.log("Identity request response:", requestText);
632
-
if (!requestRes.ok) {
634
-
const json = JSON.parse(requestText);
636
-
json.message || "Failed to request identity migration",
640
-
requestText || "Failed to request identity migration",
646
-
const jsonData = JSON.parse(requestText);
647
-
if (!jsonData.success) {
649
-
jsonData.message || "Identity migration request failed",
652
-
console.log("Identity migration requested successfully");
654
-
// Update step name to prompt for token
655
-
setSteps((prevSteps) =>
656
-
prevSteps.map((step, i) =>
661
-
"Enter the token sent to your email to complete identity migration",
666
-
// Don't continue with migration - wait for token input
669
-
console.error("Failed to parse identity request response:", e);
671
-
"Invalid response from server during identity request",
678
-
error instanceof Error ? error.message : String(error),
684
-
const startFinalization = async () => {
685
-
// Step 4: Finalize Migration
686
-
updateStepStatus(3, "in-progress");
688
-
const finalizeRes = await fetch("/api/migrate/finalize", {
690
-
headers: { "Content-Type": "application/json" },
693
-
const finalizeData = await finalizeRes.text();
694
-
if (!finalizeRes.ok) {
696
-
const json = JSON.parse(finalizeData);
697
-
throw new Error(json.message || "Failed to finalize migration");
699
-
throw new Error(finalizeData || "Failed to finalize migration");
704
-
const jsonData = JSON.parse(finalizeData);
705
-
if (!jsonData.success) {
706
-
throw new Error(jsonData.message || "Finalization failed");
709
-
throw new Error("Invalid response from server during finalization");
712
-
updateStepStatus(3, "verifying");
713
-
const verified = await verifyStep(3);
716
-
"Finalization: Verification failed, waiting for user action",
724
-
error instanceof Error ? error.message : String(error),
130
+
const allStepsCompleted = completedSteps.size === 4;
···
764
-
{steps.map((step, index) => (
765
-
<div key={step.name} class={getStepClasses(step.status)}>
766
-
{getStepIcon(step.status)}
767
-
<div class="flex-1">
769
-
class={`font-medium ${
770
-
step.status === "error"
771
-
? "text-red-900 dark:text-red-200"
772
-
: step.status === "completed"
773
-
? "text-green-900 dark:text-green-200"
774
-
: step.status === "in-progress"
775
-
? "text-blue-900 dark:text-blue-200"
776
-
: "text-gray-900 dark:text-gray-200"
779
-
{getStepDisplayName(step, index)}
783
-
<p class="text-sm text-red-600 dark:text-red-400">
786
-
const err = JSON.parse(step.error);
787
-
return err.message || step.error;
793
-
{step.isVerificationError && (
794
-
<div class="flex space-x-2 mt-2">
797
-
onClick={() => retryVerification(index)}
798
-
class="px-3 py-1 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors duration-200 dark:bg-blue-500 dark:hover:bg-blue-400"
802
-
{showContinueAnyway[index] && (
805
-
onClick={() => continueAnyway(index)}
806
-
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
807
-
dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
816
-
{index === 2 && step.status === "in-progress" &&
818
-
"Enter the token sent to your email to complete identity migration" &&
820
-
<div class="mt-4 space-y-4">
821
-
<p class="text-sm text-blue-800 dark:text-blue-200">
822
-
Please check your email for the migration token and enter
825
-
<div class="flex space-x-2">
829
-
onChange={(e) => setToken(e.currentTarget.value)}
830
-
placeholder="Enter token"
831
-
class="flex-1 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"
835
-
onClick={handleIdentityMigration}
836
-
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"
166
+
<AccountCreationStep
167
+
credentials={credentials}
168
+
onStepComplete={() => handleStepComplete(0)}
169
+
onStepError={(error, isVerificationError) =>
170
+
handleStepError(0, error, isVerificationError)}
171
+
isActive={isStepActive(0)}
175
+
credentials={credentials}
176
+
onStepComplete={() => handleStepComplete(1)}
177
+
onStepError={(error, isVerificationError) =>
178
+
handleStepError(1, error, isVerificationError)}
179
+
isActive={isStepActive(1)}
182
+
<IdentityMigrationStep
183
+
credentials={credentials}
184
+
onStepComplete={() => handleStepComplete(2)}
185
+
onStepError={(error, isVerificationError) =>
186
+
handleStepError(2, error, isVerificationError)}
187
+
isActive={isStepActive(2)}
191
+
credentials={credentials}
192
+
onStepComplete={() => handleStepComplete(3)}
193
+
onStepError={(error, isVerificationError) =>
194
+
handleStepError(3, error, isVerificationError)}
195
+
isActive={isStepActive(3)}
848
-
{steps[3].status === "completed" && (
849
-
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
850
-
<p class="text-sm text-green-800 dark:text-green-200 pb-2">
851
-
Migration completed successfully! Sign out to finish the process and
853
-
Please consider donating to Airport to support server and
856
-
<div class="flex space-x-4">
859
-
onClick={async () => {
861
-
const response = await fetch("/api/logout", {
863
-
credentials: "include",
865
-
if (!response.ok) {
866
-
throw new Error("Logout failed");
868
-
globalThis.location.href = "/";
870
-
console.error("Failed to logout:", error);
873
-
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"
878
-
stroke="currentColor"
879
-
viewBox="0 0 24 24"
882
-
stroke-linecap="round"
883
-
stroke-linejoin="round"
885
-
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"
888
-
<span>Sign Out</span>
891
-
href="https://ko-fi.com/knotbin"
893
-
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"
898
-
stroke="currentColor"
899
-
viewBox="0 0 24 24"
902
-
stroke-linecap="round"
903
-
stroke-linejoin="round"
905
-
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"
908
-
<span>Support Us</span>
199
+
<MigrationCompletion isVisible={allStepsCompleted} />