mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Feature/v2/stripeorsupabaseNotEnabled (#5006)
Removed current plan section from static plan to match connected version stripe publishable key not required to show plans or checkout in hosted version lazy load plans when needed not on load
This commit is contained in:
@@ -5569,6 +5569,7 @@
|
|||||||
},
|
},
|
||||||
"payment": {
|
"payment": {
|
||||||
"preparing": "Preparing your checkout...",
|
"preparing": "Preparing your checkout...",
|
||||||
|
"redirecting": "Redirecting to secure checkout...",
|
||||||
"upgradeTitle": "Upgrade to {{planName}}",
|
"upgradeTitle": "Upgrade to {{planName}}",
|
||||||
"success": "Payment Successful!",
|
"success": "Payment Successful!",
|
||||||
"successMessage": "Your subscription has been activated successfully. You will receive a confirmation email shortly.",
|
"successMessage": "Your subscription has been activated successfully. You will receive a confirmation email shortly.",
|
||||||
|
|||||||
+3
-3
@@ -31,9 +31,9 @@ const AdminPlanSection: React.FC = () => {
|
|||||||
|
|
||||||
// Check if we should use static version
|
// Check if we should use static version
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if Stripe is configured
|
// Only use static version if Supabase is not configured or there's an error
|
||||||
const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
// Stripe key is not required - hosted checkout works without it
|
||||||
if (!stripeKey || !isSupabaseConfigured || error) {
|
if (!isSupabaseConfigured || error) {
|
||||||
setUseStaticVersion(true);
|
setUseStaticVersion(true);
|
||||||
}
|
}
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|||||||
-40
@@ -101,46 +101,6 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({ currentLicenseInf
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||||
{/* Current Plan Section */}
|
|
||||||
<div>
|
|
||||||
<h3 style={{ margin: 0, color: 'var(--mantine-color-text)', fontSize: '1rem' }}>
|
|
||||||
{t('plan.activePlan.title', 'Active Plan')}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: '0.25rem 0 1rem 0',
|
|
||||||
color: 'var(--mantine-color-dimmed)',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('plan.activePlan.subtitle', 'Your current subscription details')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Card padding="lg" radius="md" withBorder>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Group gap="sm">
|
|
||||||
<Text size="lg" fw={600}>
|
|
||||||
{currentPlan.name}
|
|
||||||
</Text>
|
|
||||||
<Badge color="green" variant="light">
|
|
||||||
{t('subscription.status.active', 'Active')}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
{currentLicenseInfo && (
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t('plan.static.maxUsers', 'Max Users')}: {currentLicenseInfo.maxUsers}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
<div style={{ textAlign: 'right' }}>
|
|
||||||
<Text size="xl" fw={700}>
|
|
||||||
{currentPlan.price === 0 ? t('plan.free.name', 'Free') : `${currentPlan.currency}${currentPlan.price}${currentPlan.period}`}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Available Plans */}
|
{/* Available Plans */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||||
import { loadStripe } from '@stripe/stripe-js';
|
|
||||||
import licenseService from '@app/services/licenseService';
|
import licenseService from '@app/services/licenseService';
|
||||||
import { useIsMobile } from '@app/hooks/useIsMobile';
|
import { useIsMobile } from '@app/hooks/useIsMobile';
|
||||||
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
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<StripeCheckoutProps> = ({
|
const StripeCheckout: React.FC<StripeCheckoutProps> = ({
|
||||||
opened,
|
opened,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -192,25 +189,8 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
|
|||||||
|
|
||||||
// Render stage content
|
// Render stage content
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
// Check if Stripe is configured
|
// Don't block checkout - hosted mode works without publishable key
|
||||||
if (!stripePromise) {
|
// The checkout will automatically redirect to Stripe hosted page if key is missing
|
||||||
return (
|
|
||||||
<Alert color="red" title={t('payment.stripeNotConfigured', 'Stripe Not Configured')}>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Text size="sm">
|
|
||||||
{t(
|
|
||||||
'payment.stripeNotConfiguredMessage',
|
|
||||||
'Stripe payment integration is not configured. Please contact your administrator.'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<Button variant="outline" onClick={handleClose}>
|
|
||||||
{t('common.close', 'Close')}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (checkoutState.state.currentStage) {
|
switch (checkoutState.state.currentStage) {
|
||||||
case 'email':
|
case 'email':
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -35,10 +35,15 @@ export const PaymentStage: React.FC<PaymentStageProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!stripePromise) {
|
if (!stripePromise) {
|
||||||
|
// This should only happen if embedded mode was attempted without key
|
||||||
|
// Hosted checkout should have redirected before reaching this component
|
||||||
return (
|
return (
|
||||||
<Text size="sm" c="red">
|
<Stack align="center" gap="md" style={{ padding: '2rem 0' }}>
|
||||||
Stripe is not configured properly.
|
<Loader size="lg" />
|
||||||
|
<Text size="sm" c="dimmed" mt="md">
|
||||||
|
{t('payment.redirecting', 'Redirecting to secure checkout...')}
|
||||||
</Text>
|
</Text>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { usePlans } from '@app/hooks/usePlans';
|
import licenseService, { PlanTierGroup, LicenseInfo, mapLicenseToTier, PlanTier } from '@app/services/licenseService';
|
||||||
import licenseService, { PlanTierGroup, LicenseInfo, mapLicenseToTier } from '@app/services/licenseService';
|
|
||||||
import { StripeCheckout } from '@app/components/shared/stripeCheckout';
|
import { StripeCheckout } from '@app/components/shared/stripeCheckout';
|
||||||
import { userManagementService } from '@app/services/userManagementService';
|
import { userManagementService } from '@app/services/userManagementService';
|
||||||
import { alert } from '@app/components/toast';
|
import { alert } from '@app/components/toast';
|
||||||
@@ -54,8 +53,33 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
|||||||
licenseKey?: string;
|
licenseKey?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// Load plans with current currency
|
// Lazy-loaded plans state (no fetch on mount)
|
||||||
const { plans, refetch: refetchPlans } = usePlans(currentCurrency);
|
const [plans, setPlans] = useState<PlanTier[]>([]);
|
||||||
|
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
|
// Handle return from hosted Stripe checkout
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -89,6 +113,11 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
|||||||
if (activation.success) {
|
if (activation.success) {
|
||||||
console.log('License synced successfully, refreshing license context');
|
console.log('License synced successfully, refreshing license context');
|
||||||
|
|
||||||
|
// Ensure plans are loaded before using them
|
||||||
|
if (!plansLoaded) {
|
||||||
|
await fetchPlansIfNeeded(currentCurrency);
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh global license context
|
// Refresh global license context
|
||||||
await refetchLicense();
|
await refetchLicense();
|
||||||
await refetchPlans();
|
await refetchPlans();
|
||||||
@@ -135,6 +164,11 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
|||||||
if (activation.success) {
|
if (activation.success) {
|
||||||
console.log(`License key activated: ${activation.licenseType}`);
|
console.log(`License key activated: ${activation.licenseType}`);
|
||||||
|
|
||||||
|
// Ensure plans are loaded before using them
|
||||||
|
if (!plansLoaded) {
|
||||||
|
await fetchPlansIfNeeded(currentCurrency);
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh global license context
|
// Refresh global license context
|
||||||
await refetchLicense();
|
await refetchLicense();
|
||||||
await refetchPlans();
|
await refetchPlans();
|
||||||
@@ -201,7 +235,7 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleCheckoutReturn();
|
handleCheckoutReturn();
|
||||||
}, [t, refetchPlans, refetchLicense, plans]);
|
}, [t, refetchPlans, refetchLicense, plans, fetchPlansIfNeeded, plansLoaded, currentCurrency]);
|
||||||
|
|
||||||
const openCheckout = useCallback(
|
const openCheckout = useCallback(
|
||||||
async (tier: 'server' | 'enterprise', options: CheckoutOptions = {}) => {
|
async (tier: 'server' | 'enterprise', options: CheckoutOptions = {}) => {
|
||||||
@@ -217,7 +251,11 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
|||||||
const currency = options.currency || currentCurrency;
|
const currency = options.currency || currentCurrency;
|
||||||
if (currency !== currentCurrency) {
|
if (currency !== currentCurrency) {
|
||||||
setCurrentCurrency(currency);
|
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
|
// Fetch license info and user data for seat calculations
|
||||||
@@ -277,7 +315,7 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currentCurrency, plans]
|
[currentCurrency, plans, plansLoaded, fetchPlansIfNeeded]
|
||||||
);
|
);
|
||||||
|
|
||||||
const closeCheckout = useCallback(() => {
|
const closeCheckout = useCallback(() => {
|
||||||
|
|||||||
@@ -3,6 +3,16 @@
|
|||||||
* Used to decide between Embedded Checkout (HTTPS) and Hosted Checkout (HTTP)
|
* 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)
|
* Check if the current context is secure (HTTPS or localhost)
|
||||||
* @returns true if HTTPS or localhost, false if HTTP
|
* @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
|
* 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' {
|
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';
|
return isSecureContext() ? 'embedded' : 'hosted';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Embedded Checkout can be used in current context
|
* 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 {
|
export function canUseEmbeddedCheckout(): boolean {
|
||||||
return isSecureContext();
|
return isSecureContext() && isStripeConfigured();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user