"use client"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { DOCS_URLS, getProviderHelpText } from "@/lib/external-urls"; import { isCloud } from "@/lib/shared/env"; import { endActiveTour } from "@/lib/tours/use-driver-tour"; import { useOnboardingCheckpointStore } from "@/store/onboarding-checkpoint"; import { useOrgSetupStore } from "@/store/organizations/store"; import { useProviderWizardStore } from "@/store/provider-wizard/store"; import { ORG_SETUP_PHASE, ORG_WIZARD_STEP, ORGANIZATION_TYPE, OrgFlowType, OrgSetupPhase, OrgWizardStep, } from "@/types/organizations"; import { PROVIDER_WIZARD_MODE, PROVIDER_WIZARD_STEP, ProviderWizardStep, } from "@/types/provider-wizard"; import { ProviderType } from "@/types/providers"; import { getProviderWizardModalTitle } from "../provider-wizard-modal.utils"; import { WIZARD_FOOTER_ACTION_TYPE, WizardFooterConfig, } from "../steps/footer-controls"; import type { OrgWizardInitialData, ProviderWizardInitialData } from "../types"; const WIZARD_VARIANT = { PROVIDER: "provider", ORGANIZATIONS: "organizations", } as const; type WizardVariant = (typeof WIZARD_VARIANT)[keyof typeof WIZARD_VARIANT]; const ORG_DOCS_URL = { [ORGANIZATION_TYPE.AWS]: DOCS_URLS.AWS_ORGANIZATIONS, [ORGANIZATION_TYPE.AZURE]: DOCS_URLS.AZURE_ORGANIZATIONS, [ORGANIZATION_TYPE.GCP]: DOCS_URLS.GCP_ORGANIZATIONS, } as const satisfies Record; const EMPTY_FOOTER_CONFIG: WizardFooterConfig = { showBack: false, backLabel: "Back", showSecondaryAction: false, secondaryActionLabel: "", secondaryActionVariant: "outline", secondaryActionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, showAction: false, actionLabel: "Next", actionType: WIZARD_FOOTER_ACTION_TYPE.BUTTON, }; interface UseProviderWizardControllerProps { open: boolean; onOpenChange: (open: boolean) => void; initialData?: ProviderWizardInitialData; orgInitialData?: OrgWizardInitialData; // When false, skips post-close router.refresh() — caller relies on revalidatePath instead. refreshOnClose?: boolean; } export function useProviderWizardController({ open, onOpenChange, initialData, orgInitialData, refreshOnClose = true, }: UseProviderWizardControllerProps) { const router = useRouter(); const initialProviderId = initialData?.providerId ?? null; const initialProviderType = initialData?.providerType ?? null; const initialProviderUid = initialData?.providerUid ?? null; const initialProviderAlias = initialData?.providerAlias ?? null; const initialSecretId = initialData?.secretId ?? null; const initialVia = initialData?.via ?? null; const initialMode = initialData?.mode ?? null; const hasHydratedForCurrentOpenRef = useRef(false); const [wizardVariant, setWizardVariant] = useState( WIZARD_VARIANT.PROVIDER, ); const [currentStep, setCurrentStep] = useState( PROVIDER_WIZARD_STEP.CONNECT, ); const [orgCurrentStep, setOrgCurrentStep] = useState( ORG_WIZARD_STEP.SETUP, ); const [footerConfig, setFooterConfig] = useState(EMPTY_FOOTER_CONFIG); const [providerTypeHint, setProviderTypeHint] = useState( null, ); const [orgSetupPhase, setOrgSetupPhase] = useState( ORG_SETUP_PHASE.DETAILS, ); const { reset: resetProviderWizard, setProvider, setVia, setSecretId, setMode, mode, providerType, via, } = useProviderWizardStore(); const { reset: resetOrgWizard, setOrganization, setOrganizationType, organizationType, } = useOrgSetupStore(); useEffect(() => { if (!open) { hasHydratedForCurrentOpenRef.current = false; return; } if (hasHydratedForCurrentOpenRef.current) { return; } hasHydratedForCurrentOpenRef.current = true; if (orgInitialData) { setWizardVariant(WIZARD_VARIANT.ORGANIZATIONS); resetOrgWizard(); setOrganizationType(orgInitialData.organizationType); setOrganization( orgInitialData.organizationId, orgInitialData.organizationName, orgInitialData.externalId, ); setOrgCurrentStep(orgInitialData.targetStep); setOrgSetupPhase(orgInitialData.targetPhase); setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); return; } if (initialProviderId && initialProviderType && initialProviderUid) { setWizardVariant(WIZARD_VARIANT.PROVIDER); setProvider({ id: initialProviderId, type: initialProviderType, uid: initialProviderUid, alias: initialProviderAlias, }); setVia(initialVia); setSecretId(initialSecretId); setMode( initialMode || (initialSecretId ? PROVIDER_WIZARD_MODE.UPDATE : PROVIDER_WIZARD_MODE.ADD), ); setCurrentStep(PROVIDER_WIZARD_STEP.CREDENTIALS); setOrgCurrentStep(ORG_WIZARD_STEP.SETUP); setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(initialProviderType); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); return; } resetProviderWizard(); resetOrgWizard(); setWizardVariant(WIZARD_VARIANT.PROVIDER); setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT); setOrgCurrentStep(ORG_WIZARD_STEP.SETUP); setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); }, [ initialMode, initialProviderAlias, initialProviderId, initialProviderType, initialProviderUid, initialSecretId, initialVia, open, orgInitialData, resetOrgWizard, resetProviderWizard, setMode, setOrganization, setOrganizationType, setProvider, setSecretId, setVia, ]); const isOrgDirectEntry = Boolean(orgInitialData); const handleClose = () => { // Closing the wizard at any point ends the add-provider tour; the checkpoint // logic below still drives the handoff to scans. No-op off-onboarding. endActiveTour(); // Read providerId before reset clears it — non-null means a provider was connected. const connectedProviderId = useProviderWizardStore.getState().providerId; resetProviderWizard(); resetOrgWizard(); setWizardVariant(WIZARD_VARIANT.PROVIDER); setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT); setOrgCurrentStep(ORG_WIZARD_STEP.SETUP); setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); onOpenChange(false); // Cloud-only; only fires if the store is armed (user started onboarding). if (isCloud()) { useOnboardingCheckpointStore.getState().requestOpenOnWizardClose({ providerConnected: connectedProviderId !== null, }); } if (refreshOnClose) { router.refresh(); } }; const handleDialogOpenChange = (nextOpen: boolean) => { if (nextOpen) { onOpenChange(true); return; } handleClose(); }; const handleTestSuccess = () => { if ( useProviderWizardStore.getState().mode === PROVIDER_WIZARD_MODE.UPDATE ) { handleClose(); return; } setCurrentStep(PROVIDER_WIZARD_STEP.LAUNCH); }; const openOrganizationsFlow = ( orgType: OrgFlowType = ORGANIZATION_TYPE.AWS, ) => { // Organizations diverges from the credentials path the tour guides toward; end // it so it doesn't dangle on a step that no longer fits. No-op off-onboarding. endActiveTour(); resetOrgWizard(); setOrganizationType(orgType); setWizardVariant(WIZARD_VARIANT.ORGANIZATIONS); setOrgCurrentStep(ORG_WIZARD_STEP.SETUP); setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); }; const backToProviderFlow = () => { resetOrgWizard(); setWizardVariant(WIZARD_VARIANT.PROVIDER); setCurrentStep(PROVIDER_WIZARD_STEP.CONNECT); setFooterConfig(EMPTY_FOOTER_CONFIG); setProviderTypeHint(null); setOrgSetupPhase(ORG_SETUP_PHASE.DETAILS); }; const isProviderFlow = wizardVariant === WIZARD_VARIANT.PROVIDER; const docsLink = isProviderFlow ? getProviderHelpText( providerTypeHint ?? providerType ?? "", currentStep, via, ).link : ORG_DOCS_URL[organizationType]; const resolvedFooterConfig: WizardFooterConfig = footerConfig; const modalTitle = getProviderWizardModalTitle(mode); return { currentStep, docsLink, footerConfig, handleClose, handleDialogOpenChange, handleTestSuccess, isOrgDirectEntry, isProviderFlow, mode, modalTitle, openOrganizationsFlow, organizationType, orgCurrentStep, orgSetupPhase, providerTypeHint, resolvedFooterConfig, setCurrentStep, setFooterConfig, setOrgCurrentStep, setOrgSetupPhase, setProviderTypeHint, backToProviderFlow, wizardVariant, }; }