diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index da2ee3623..dea5ff141 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -5569,6 +5569,7 @@ }, "payment": { "preparing": "Preparing your checkout...", + "redirecting": "Redirecting to secure checkout...", "upgradeTitle": "Upgrade to {{planName}}", "success": "Payment Successful!", "successMessage": "Your subscription has been activated successfully. You will receive a confirmation email shortly.", diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx index 52e20a6d0..c5d1ff658 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx @@ -31,9 +31,9 @@ const AdminPlanSection: React.FC = () => { // Check if we should use static version useEffect(() => { - // Check if Stripe is configured - const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY; - if (!stripeKey || !isSupabaseConfigured || error) { + // Only use static version if Supabase is not configured or there's an error + // Stripe key is not required - hosted checkout works without it + if (!isSupabaseConfigured || error) { setUseStaticVersion(true); } }, [error]); diff --git a/frontend/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx index 3bdd06baf..ba937263a 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/plan/StaticPlanSection.tsx @@ -101,46 +101,6 @@ const StaticPlanSection: React.FC = ({ currentLicenseInf return (
- {/* Current Plan Section */} -
-

- {t('plan.activePlan.title', 'Active Plan')} -

-

- {t('plan.activePlan.subtitle', 'Your current subscription details')} -

- - - - - - - {currentPlan.name} - - - {t('subscription.status.active', 'Active')} - - - {currentLicenseInfo && ( - - {t('plan.static.maxUsers', 'Max Users')}: {currentLicenseInfo.maxUsers} - - )} - -
- - {currentPlan.price === 0 ? t('plan.free.name', 'Free') : `${currentPlan.currency}${currentPlan.price}${currentPlan.period}`} - -
-
-
-
{/* Available Plans */}
diff --git a/frontend/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx b/frontend/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx index e2aa7f540..2743a4906 100644 --- a/frontend/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx +++ b/frontend/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx @@ -1,8 +1,7 @@ import React, { useEffect } from 'react'; -import { Modal, Text, Alert, Stack, Button, Group, ActionIcon } from '@mantine/core'; +import { Modal, Text, Group, ActionIcon } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import LocalIcon from '@app/components/shared/LocalIcon'; -import { loadStripe } from '@stripe/stripe-js'; import licenseService from '@app/services/licenseService'; import { useIsMobile } from '@app/hooks/useIsMobile'; import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex'; @@ -37,8 +36,6 @@ if (STRIPE_KEY && !STRIPE_KEY.startsWith('pk_')) { ); } -const stripePromise = STRIPE_KEY ? loadStripe(STRIPE_KEY) : null; - const StripeCheckout: React.FC = ({ opened, onClose, @@ -192,25 +189,8 @@ const StripeCheckout: React.FC = ({ // Render stage content const renderContent = () => { - // Check if Stripe is configured - if (!stripePromise) { - return ( - - - - {t( - 'payment.stripeNotConfiguredMessage', - 'Stripe payment integration is not configured. Please contact your administrator.' - )} - - - - - ); - } - + // Don't block checkout - hosted mode works without publishable key + // The checkout will automatically redirect to Stripe hosted page if key is missing switch (checkoutState.state.currentStage) { case 'email': return ( diff --git a/frontend/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.tsx b/frontend/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.tsx index 9cee1983c..84664f873 100644 --- a/frontend/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.tsx +++ b/frontend/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.tsx @@ -35,10 +35,15 @@ export const PaymentStage: React.FC = ({ } if (!stripePromise) { + // This should only happen if embedded mode was attempted without key + // Hosted checkout should have redirected before reaching this component return ( - - Stripe is not configured properly. - + + + + {t('payment.redirecting', 'Redirecting to secure checkout...')} + + ); } diff --git a/frontend/src/proprietary/contexts/CheckoutContext.tsx b/frontend/src/proprietary/contexts/CheckoutContext.tsx index f8c544934..d6c6db86c 100644 --- a/frontend/src/proprietary/contexts/CheckoutContext.tsx +++ b/frontend/src/proprietary/contexts/CheckoutContext.tsx @@ -1,7 +1,6 @@ import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; -import { usePlans } from '@app/hooks/usePlans'; -import licenseService, { PlanTierGroup, LicenseInfo, mapLicenseToTier } from '@app/services/licenseService'; +import licenseService, { PlanTierGroup, LicenseInfo, mapLicenseToTier, PlanTier } from '@app/services/licenseService'; import { StripeCheckout } from '@app/components/shared/stripeCheckout'; import { userManagementService } from '@app/services/userManagementService'; import { alert } from '@app/components/toast'; @@ -54,8 +53,33 @@ export const CheckoutProvider: React.FC = ({ licenseKey?: string; } | null>(null); - // Load plans with current currency - const { plans, refetch: refetchPlans } = usePlans(currentCurrency); + // Lazy-loaded plans state (no fetch on mount) + const [plans, setPlans] = useState([]); + const [plansLoaded, setPlansLoaded] = useState(false); + const [plansLoading, setPlansLoading] = useState(false); + + // Lazy fetch plans only when needed + const fetchPlansIfNeeded = useCallback(async (currency: string) => { + // Don't fetch if already loading + if (plansLoading) return; + + try { + setPlansLoading(true); + const response = await licenseService.getPlans(currency); + setPlans(response.plans); + setPlansLoaded(true); + } catch (error) { + console.error('Failed to fetch plans:', error); + // Don't block - let components handle the error + } finally { + setPlansLoading(false); + } + }, [plansLoading]); + + const refetchPlans = useCallback(() => { + setPlansLoaded(false); // Force refetch + return fetchPlansIfNeeded(currentCurrency); + }, [currentCurrency, fetchPlansIfNeeded]); // Handle return from hosted Stripe checkout useEffect(() => { @@ -89,6 +113,11 @@ export const CheckoutProvider: React.FC = ({ if (activation.success) { console.log('License synced successfully, refreshing license context'); + // Ensure plans are loaded before using them + if (!plansLoaded) { + await fetchPlansIfNeeded(currentCurrency); + } + // Refresh global license context await refetchLicense(); await refetchPlans(); @@ -135,6 +164,11 @@ export const CheckoutProvider: React.FC = ({ if (activation.success) { console.log(`License key activated: ${activation.licenseType}`); + // Ensure plans are loaded before using them + if (!plansLoaded) { + await fetchPlansIfNeeded(currentCurrency); + } + // Refresh global license context await refetchLicense(); await refetchPlans(); @@ -201,7 +235,7 @@ export const CheckoutProvider: React.FC = ({ }; handleCheckoutReturn(); - }, [t, refetchPlans, refetchLicense, plans]); + }, [t, refetchPlans, refetchLicense, plans, fetchPlansIfNeeded, plansLoaded, currentCurrency]); const openCheckout = useCallback( async (tier: 'server' | 'enterprise', options: CheckoutOptions = {}) => { @@ -217,7 +251,11 @@ export const CheckoutProvider: React.FC = ({ const currency = options.currency || currentCurrency; if (currency !== currentCurrency) { setCurrentCurrency(currency); - // Plans will reload automatically via usePlans + } + + // Fetch plans if not already loaded + if (!plansLoaded) { + await fetchPlansIfNeeded(currency); } // Fetch license info and user data for seat calculations @@ -277,7 +315,7 @@ export const CheckoutProvider: React.FC = ({ setIsLoading(false); } }, - [currentCurrency, plans] + [currentCurrency, plans, plansLoaded, fetchPlansIfNeeded] ); const closeCheckout = useCallback(() => { diff --git a/frontend/src/proprietary/utils/protocolDetection.ts b/frontend/src/proprietary/utils/protocolDetection.ts index eb5d0a29a..4f7e328c5 100644 --- a/frontend/src/proprietary/utils/protocolDetection.ts +++ b/frontend/src/proprietary/utils/protocolDetection.ts @@ -3,6 +3,16 @@ * Used to decide between Embedded Checkout (HTTPS) and Hosted Checkout (HTTP) */ +/** + * Check if Stripe publishable key is configured + * Similar to isSupabaseConfigured pattern - checks availability at decision points + * @returns true if key exists and has valid format + */ +export function isStripeConfigured(): boolean { + const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY; + return !!stripeKey && stripeKey.startsWith('pk_'); +} + /** * Check if the current context is secure (HTTPS or localhost) * @returns true if HTTPS or localhost, false if HTTP @@ -28,16 +38,24 @@ export function isSecureContext(): boolean { /** * Get the appropriate Stripe checkout UI mode based on current context - * @returns 'embedded' for HTTPS/localhost, 'hosted' for HTTP + * @returns 'embedded' for HTTPS with key, 'hosted' for HTTP or missing key */ export function getCheckoutMode(): 'embedded' | 'hosted' { + // Force hosted checkout if no publishable key (regardless of protocol) + // Hosted checkout works without the key - it just redirects to Stripe + if (!isStripeConfigured()) { + return 'hosted'; + } + + // Normal protocol-based detection if key is available return isSecureContext() ? 'embedded' : 'hosted'; } /** * Check if Embedded Checkout can be used in current context - * @returns true if secure context (HTTPS/localhost) + * Requires both HTTPS and Stripe publishable key + * @returns true if secure context AND key is configured */ export function canUseEmbeddedCheckout(): boolean { - return isSecureContext(); + return isSecureContext() && isStripeConfigured(); }