import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from "react"; import { useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import licenseService from "@app/services/licenseService"; import UpdateSeatsModal from "@app/components/shared/UpdateSeatsModal"; import { userManagementService } from "@app/services/userManagementService"; import { alert } from "@app/components/toast"; import { useOptionalLicense } from "@app/contexts/LicenseContext"; import { resyncExistingLicense } from "@app/utils/licenseCheckoutUtils"; export interface UpdateSeatsOptions { onSuccess?: () => void; onError?: (error: string) => void; } interface UpdateSeatsContextValue { openUpdateSeats: (options?: UpdateSeatsOptions) => Promise; closeUpdateSeats: () => void; isOpen: boolean; isLoading: boolean; } const UpdateSeatsContext = createContext(undefined); interface UpdateSeatsProviderProps { children: ReactNode; } export const UpdateSeatsProvider: React.FC = ({ children }) => { const { t } = useTranslation(); const location = useLocation(); // Use optional hook - won't throw during setup wizard when license provider isn't needed const license = useOptionalLicense(); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [currentSeats, setCurrentSeats] = useState(1); const [minimumSeats, setMinimumSeats] = useState(1); const [currentOptions, setCurrentOptions] = useState({}); // Handle return from Stripe billing portal useEffect(() => { // CRITICAL FIX: Don't run billing check on auth routes to prevent race conditions // during SAML/OAuth callback. This check only matters after successful billing // portal redirects, which never happen on auth routes. const isAuthRoute = location.pathname === "/login" || location.pathname === "/signup" || location.pathname === "/auth/callback" || location.pathname.startsWith("/invite/"); if (isAuthRoute) { console.log("[UpdateSeatsContext] On auth route, skipping billing return check"); return; } const handleBillingReturn = async () => { const urlParams = new URLSearchParams(window.location.search); const seatsUpdated = urlParams.get("seats_updated"); if (seatsUpdated === "true") { console.log("Seats updated successfully, syncing license with Keygen"); // Clear URL parameters window.history.replaceState({}, "", window.location.pathname); try { // Wait a moment for Stripe webhook to process await new Promise((resolve) => setTimeout(resolve, 2000)); // Resync license with Keygen (not just local fetch) console.log("Seat update detected - resyncing license with Keygen"); const activation = await resyncExistingLicense(); if (activation.success) { console.log("License synced successfully after seat update"); // Refresh global license context await license?.refetchLicense(); // Get updated license info for notification const updatedLicense = await licenseService.getLicenseInfo(); alert({ alertType: "success", title: t("billing.seatsUpdated", "Seats Updated"), body: t("billing.seatsUpdatedMessage", "Your enterprise seats have been updated to {{seats}}", { seats: updatedLicense.maxUsers, }), }); } else { throw new Error(activation.error || "Failed to sync license"); } } catch (error) { console.error("Failed to sync license after seat update:", error); alert({ alertType: "warning", title: t("billing.updateProcessing", "Update Processing"), body: t( "billing.updateProcessingMessage", "Your seat update is being processed. Please refresh in a few moments.", ), }); } } }; // CRITICAL FIX: Properly handle async function and catch errors handleBillingReturn().catch((error) => { console.error("[UpdateSeatsContext] Error in billing return handler:", error); // Don't throw - this is initialization, should not block rendering }); }, [t, location.pathname, license]); const openUpdateSeats = useCallback( async (options: UpdateSeatsOptions = {}) => { try { setIsLoading(true); // Fetch current license info and user count const [licenseInfo, userData] = await Promise.all([licenseService.getLicenseInfo(), userManagementService.getUsers()]); // Validate this is an enterprise license if (!licenseInfo || licenseInfo.licenseType !== "ENTERPRISE") { throw new Error(t("billing.notEnterprise", "Seat management is only available for enterprise licenses")); } const currentLicenseSeats = licenseInfo.maxUsers || 1; const currentUserCount = userData.totalUsers || 0; // Minimum seats must be at least the current number of users const calculatedMinSeats = Math.max(currentUserCount, 1); console.log( `Opening seat update: current seats=${currentLicenseSeats}, current users=${currentUserCount}, minimum=${calculatedMinSeats}`, ); setCurrentSeats(currentLicenseSeats); setMinimumSeats(calculatedMinSeats); setCurrentOptions(options); setIsOpen(true); } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to open seat update"; console.error("Error opening seat update:", errorMessage); alert({ alertType: "error", title: t("common.error", "Error"), body: errorMessage, }); options.onError?.(errorMessage); } finally { setIsLoading(false); } }, [t], ); const closeUpdateSeats = useCallback(() => { setIsOpen(false); setCurrentOptions({}); // Refetch license after modal closes to update UI license?.refetchLicense(); }, [license]); const handleUpdateSeats = useCallback( async (newSeatCount: number): Promise => { try { // Get current license key const licenseInfo = await licenseService.getLicenseInfo(); if (!licenseInfo?.licenseKey) { throw new Error("No license key found"); } console.log(`Updating seats from ${currentSeats} to ${newSeatCount}`); // Call manage-billing function with new seat count const portalUrl = await licenseService.updateEnterpriseSeats(newSeatCount, licenseInfo.licenseKey); return portalUrl; } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to update seats"; console.error("Error updating seats:", errorMessage); currentOptions.onError?.(errorMessage); throw err; } }, [currentSeats, currentOptions], ); const handleSuccess = useCallback(() => { console.log("Seat update initiated successfully"); currentOptions.onSuccess?.(); }, [currentOptions]); const handleError = useCallback( (error: string) => { console.error("Seat update error:", error); currentOptions.onError?.(error); }, [currentOptions], ); return ( {children} ); }; export const useUpdateSeats = (): UpdateSeatsContextValue => { const context = useContext(UpdateSeatsContext); if (!context) { throw new Error("useUpdateSeats must be used within an UpdateSeatsProvider"); } return context; }; export default UpdateSeatsContext;