mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7167e5d7b | ||
|
|
d542eae79e | ||
|
|
a98289d4c1 | ||
|
|
14f5f34950 | ||
|
|
e338076e6e | ||
|
|
ecd44c6b8f | ||
|
|
e3f2d7a376 |
@@ -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.",
|
||||
|
||||
+3
-3
@@ -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]);
|
||||
|
||||
-40
@@ -101,46 +101,6 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({ currentLicenseInf
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<div>
|
||||
|
||||
@@ -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<StripeCheckoutProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
@@ -192,25 +189,8 @@ const StripeCheckout: React.FC<StripeCheckoutProps> = ({
|
||||
|
||||
// Render stage content
|
||||
const renderContent = () => {
|
||||
// Check if Stripe is configured
|
||||
if (!stripePromise) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
|
||||
@@ -35,10 +35,15 @@ export const PaymentStage: React.FC<PaymentStageProps> = ({
|
||||
}
|
||||
|
||||
if (!stripePromise) {
|
||||
// This should only happen if embedded mode was attempted without key
|
||||
// Hosted checkout should have redirected before reaching this component
|
||||
return (
|
||||
<Text size="sm" c="red">
|
||||
Stripe is not configured properly.
|
||||
</Text>
|
||||
<Stack align="center" gap="md" style={{ padding: '2rem 0' }}>
|
||||
<Loader size="lg" />
|
||||
<Text size="sm" c="dimmed" mt="md">
|
||||
{t('payment.redirecting', 'Redirecting to secure checkout...')}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<CheckoutProviderProps> = ({
|
||||
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<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
|
||||
useEffect(() => {
|
||||
@@ -89,6 +113,11 @@ export const CheckoutProvider: React.FC<CheckoutProviderProps> = ({
|
||||
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<CheckoutProviderProps> = ({
|
||||
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<CheckoutProviderProps> = ({
|
||||
};
|
||||
|
||||
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<CheckoutProviderProps> = ({
|
||||
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<CheckoutProviderProps> = ({
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[currentCurrency, plans]
|
||||
[currentCurrency, plans, plansLoaded, fetchPlansIfNeeded]
|
||||
);
|
||||
|
||||
const closeCheckout = useCallback(() => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user