import { useReducer, useCallback, useRef, useEffect, useState } from "react"; import { useTranslate } from "ra-core"; import { getIsInitialized } from "@/components/atomic-crm/providers/supabase/authProvider"; import { saveSupabaseConfig, validateSupabaseConnection, } from "@/lib/supabase-config"; import { SetupWizardProps, SSEEvent, ProvisioningResult } from "./types"; import { wizardReducer, initialState } from "./reducer"; import { normalizeSupabaseUrl, validateAccessToken, extractProjectId, } from "./validators"; import { fetchOrganizations, autoProvisionProject, runMigration, SetupApiError, } from "./api"; // Step components import { WelcomeStep } from "./steps/WelcomeStep"; import { TypeStep } from "./steps/TypeStep"; import { ManagedTokenStep } from "./steps/ManagedTokenStep"; import { ManagedOrgStep } from "./steps/ManagedOrgStep"; import { ProvisioningStep } from "./steps/ProvisioningStep"; import { CredentialsStep } from "./steps/CredentialsStep"; import { MigrationStep } from "./steps/MigrationStep"; export function SetupWizard({ onComplete, open = true }: SetupWizardProps) { const [state, dispatch] = useReducer(wizardReducer, initialState); const translate = useTranslate(); // Secure refs for sensitive data const accessTokenRef = useRef(""); const dbPassRef = useRef(""); const [manualAccessToken, setManualAccessToken] = useState(""); useEffect(() => { return () => { accessTokenRef.current = ""; dbPassRef.current = ""; setManualAccessToken(""); }; }, []); const addLog = useCallback( ( type: "info" | "error" | "success" | "stdout" | "stderr", message: string, ) => { dispatch({ type: "ADD_LOG", payload: { type, message } }); }, [], ); const handleFetchOrgs = useCallback(async () => { dispatch({ type: "SET_ERROR", payload: null }); dispatch({ type: "SET_FETCHING_ORGS", payload: true }); const token = state.managed.accessToken.trim(); const validation = validateAccessToken(token); if (!validation.valid) { dispatch({ type: "SET_ERROR", payload: validation.message || "Invalid token", }); dispatch({ type: "SET_FETCHING_ORGS", payload: false }); return; } try { accessTokenRef.current = token; const organizations = await fetchOrganizations(token); dispatch({ type: "SET_ORGANIZATIONS", payload: organizations }); dispatch({ type: "SET_STEP", payload: "managed-org" }); } catch (err) { const error = err instanceof SetupApiError ? err : new Error(String(err)); dispatch({ type: "SET_ERROR", payload: error.message }); } finally { dispatch({ type: "SET_FETCHING_ORGS", payload: false }); } }, [state.managed.accessToken]); const handleAutoProvision = useCallback(async () => { dispatch({ type: "SET_ERROR", payload: null }); dispatch({ type: "SET_STEP", payload: "provisioning" }); dispatch({ type: "CLEAR_LOGS" }); dispatch({ type: "SET_PROVISIONING", payload: true }); addLog("info", translate("setup.connectingProvisioning")); const token = accessTokenRef.current || state.managed.accessToken.trim(); let provisionResult: ProvisioningResult | null = null; try { await autoProvisionProject( { accessToken: token, orgId: state.managed.selectedOrgId!, projectName: state.managed.projectName, region: state.managed.region, }, (event: SSEEvent) => { switch (event.type) { case "info": addLog("info", event.data); break; case "project_id": dispatch({ type: "SET_MANUAL_PROJECT_ID", payload: event.data }); break; case "success": provisionResult = event.data as ProvisioningResult; addLog("success", translate("setup.projectReady")); // Save config immediately saveSupabaseConfig({ url: normalizeSupabaseUrl(provisionResult.url), anonKey: provisionResult.anonKey.trim(), }); dispatch({ type: "SET_PROVISIONING_RESULT", payload: provisionResult, }); dbPassRef.current = provisionResult.dbPass; break; case "error": dispatch({ type: "SET_ERROR", payload: event.data }); addLog("error", event.data); break; case "done": dispatch({ type: "SET_PROVISIONING", payload: false }); if (event.data === "failed") { dispatch({ type: "SET_ERROR", payload: translate("setup.provisioningFailedDetails"), }); } break; } }, ); dispatch({ type: "SET_PROVISIONING", payload: false }); // After provisioning, run migration if (provisionResult) { await handleRunMigration(provisionResult.projectId, token); } } catch (err) { const error = err instanceof SetupApiError ? err : new Error(String(err)); dispatch({ type: "SET_ERROR", payload: error.message }); dispatch({ type: "SET_PROVISIONING", payload: false }); addLog("error", `Provisioning failed: ${error.message}`); } }, [ state.managed.selectedOrgId, state.managed.projectName, state.managed.region, state.managed.accessToken, addLog, ]); const handleRunMigration = useCallback( async (overrideProjectId?: string, overrideToken?: string) => { const targetProjectId = overrideProjectId || state.manual.projectId || extractProjectId(state.manual.url); const targetToken = overrideToken || accessTokenRef.current; if (!targetProjectId) { dispatch({ type: "SET_ERROR", payload: translate("setup.projectIdRequired"), }); return; } dispatch({ type: "SET_STEP", payload: "migration" }); dispatch({ type: "CLEAR_LOGS" }); dispatch({ type: "SET_MIGRATING", payload: true }); addLog("info", translate("setup.startingMigration")); try { await runMigration( { projectRef: targetProjectId, accessToken: targetToken, dbPassword: dbPassRef.current, }, (event: SSEEvent) => { switch (event.type) { case "stdout": case "info": addLog("info", event.data); break; case "stderr": case "error": addLog("error", event.data); break; case "success": addLog("success", event.data); break; } }, ); dispatch({ type: "SET_MIGRATING", payload: false }); dispatch({ type: "SET_MIGRATION_COMPLETE", payload: true }); dispatch({ type: "SET_STEP", payload: "success" }); addLog("success", translate("setup.setupCompleteLog")); // Clear init_state cache so the app re-checks if database is initialized getIsInitialized.clearCache(); // Complete setup - reload page to reinitialize all modules with new credentials setTimeout(() => { window.location.reload(); }, 1500); } catch (err) { const error = err instanceof SetupApiError ? err : new Error(String(err)); dispatch({ type: "SET_ERROR", payload: error.message }); dispatch({ type: "SET_MIGRATING", payload: false }); addLog("error", `Migration failed: ${error.message}`); } }, [state.manual.projectId, state.manual.url, addLog, onComplete], ); const handleManualSetup = useCallback(async () => { dispatch({ type: "SET_ERROR", payload: null }); dispatch({ type: "SET_VALIDATING", payload: true }); const url = normalizeSupabaseUrl(state.manual.url); const anonKey = state.manual.anonKey.trim(); try { const isValid = await validateSupabaseConnection(url, anonKey); if (!isValid) { throw new Error( "Failed to connect to Supabase. Please check your credentials.", ); } saveSupabaseConfig({ url, anonKey }); const projectId = extractProjectId(url); dispatch({ type: "SET_MANUAL_PROJECT_ID", payload: projectId }); dispatch({ type: "SET_VALIDATING", payload: false }); // Check migration status and init_state addLog("info", translate("setup.checkingDatabase")); // Create a fresh Supabase client with the new credentials // (the global singleton hasn't been reinitialized yet) const { createClient } = await import("@supabase/supabase-js"); const freshSupabase = createClient(url, anonKey); const { checkMigrationStatus } = await import("@/lib/migration-check"); const migrationStatus = await checkMigrationStatus(freshSupabase); const needsMigration = migrationStatus.needsMigration; // Check if database is initialized const { data: initData } = await freshSupabase .from("init_state") .select("is_initialized") .maybeSingle(); const isInitialized = initData?.is_initialized > 0; addLog( "info", `Database version: ${migrationStatus.dbVersion || "none"}`, ); addLog("info", `App version: ${migrationStatus.appVersion}`); addLog("info", `Needs migration: ${needsMigration}`); addLog("info", `Is initialized: ${isInitialized}`); // Decide what to do based on state if (!isInitialized) { // Database has no users - need migration to set up schema and backfill addLog("info", translate("setup.dbNotInitialized")); dispatch({ type: "SET_STEP", payload: "migration" }); } else if ( needsMigration && migrationStatus.dbVersion !== migrationStatus.appVersion ) { // Database has users but is on an older version - offer migration addLog( "info", translate("setup.dbVersionBehind", { dbVersion: migrationStatus.dbVersion, appVersion: migrationStatus.appVersion, }), ); dispatch({ type: "SET_STEP", payload: "migration" }); } else if ( needsMigration && migrationStatus.dbVersion === migrationStatus.appVersion ) { // Same version but legacy schema (no timestamp tracking) // Skip migration to avoid disrupting working database addLog("info", translate("setup.dbLegacySchema")); addLog("success", translate("setup.skippingMigration")); addLog("info", translate("setup.reloadingApp")); getIsInitialized.clearCache(); setTimeout(() => { window.location.reload(); }, 1000); } else { // Database is up to date and initialized - complete setup addLog("success", translate("setup.dbReady")); addLog("info", translate("setup.reloadingApp")); getIsInitialized.clearCache(); setTimeout(() => { window.location.reload(); }, 1000); } } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); dispatch({ type: "SET_ERROR", payload: error.message }); dispatch({ type: "SET_VALIDATING", payload: false }); } }, [state.manual.url, state.manual.anonKey, addLog, onComplete]); const renderStep = () => { switch (state.step) { case "welcome": return ( dispatch({ type: "SET_STEP", payload: "type" })} /> ); case "type": return ( { dispatch({ type: "SET_SETUP_TYPE", payload: "managed" }); dispatch({ type: "SET_STEP", payload: "managed-token" }); }} onManual={() => { dispatch({ type: "SET_SETUP_TYPE", payload: "manual" }); dispatch({ type: "SET_STEP", payload: "credentials" }); }} onBack={() => dispatch({ type: "SET_STEP", payload: "welcome" })} /> ); case "managed-token": return ( dispatch({ type: "SET_ACCESS_TOKEN", payload: value }) } onFetchOrgs={handleFetchOrgs} onBack={() => dispatch({ type: "SET_STEP", payload: "type" })} error={state.error} isFetching={state.fetchingOrgs} /> ); case "managed-org": return ( dispatch({ type: "SET_SELECTED_ORG", payload: value }) } onProjectNameChange={(value) => dispatch({ type: "SET_PROJECT_NAME", payload: value }) } onRegionChange={(value) => dispatch({ type: "SET_REGION", payload: value }) } onProvision={handleAutoProvision} onBack={() => dispatch({ type: "SET_STEP", payload: "managed-token" }) } /> ); case "provisioning": return ( { // Go back to org selection to try again dispatch({ type: "SET_ERROR", payload: null }); dispatch({ type: "SET_STEP", payload: "managed-org" }); }} onSwitchToManual={() => { // Switch to manual connect flow dispatch({ type: "SET_ERROR", payload: null }); dispatch({ type: "SET_SETUP_TYPE", payload: "manual" }); dispatch({ type: "SET_STEP", payload: "credentials" }); }} /> ); case "credentials": return ( dispatch({ type: "SET_MANUAL_URL", payload: value }) } onAnonKeyChange={(value) => dispatch({ type: "SET_MANUAL_ANON_KEY", payload: value }) } onSave={handleManualSetup} onBack={() => dispatch({ type: "SET_STEP", payload: "type" })} error={state.error} /> ); case "migration": return ( { const projectId = state.manual.projectId || extractProjectId(state.manual.url); const token = state.setupType === "managed" ? accessTokenRef.current || state.managed.accessToken : manualAccessToken; handleRunMigration(projectId, token); }} /> ); case "success": return (
🎉

{translate("setup.setupComplete")}

{translate("setup.redirecting")}

); default: return null; } }; if (!open) return null; return (
{renderStep()}
); }