diff --git a/frontend/src/proprietary/components/AppProviders.tsx b/frontend/src/proprietary/components/AppProviders.tsx
index b4839fbba..a02497179 100644
--- a/frontend/src/proprietary/components/AppProviders.tsx
+++ b/frontend/src/proprietary/components/AppProviders.tsx
@@ -1,6 +1,7 @@
import { AppProviders as CoreAppProviders, AppProvidersProps } from "@core/components/AppProviders";
import { AuthProvider } from "@app/auth/UseSession";
import { CheckoutProvider } from "@app/contexts/CheckoutContext";
+import UpgradeBanner from "@app/components/shared/UpgradeBanner";
export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) {
return (
@@ -10,6 +11,7 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
>
+
{children}
diff --git a/frontend/src/proprietary/components/shared/ManageBillingButton.tsx b/frontend/src/proprietary/components/shared/ManageBillingButton.tsx
index 3cbc07f4c..fc523f62f 100644
--- a/frontend/src/proprietary/components/shared/ManageBillingButton.tsx
+++ b/frontend/src/proprietary/components/shared/ManageBillingButton.tsx
@@ -17,13 +17,29 @@ export const ManageBillingButton: React.FC = ({
const handleClick = async () => {
try {
setLoading(true);
- const response = await licenseService.createBillingPortalSession(returnUrl);
- window.location.href = response.url;
- } catch (error) {
+
+ // Get current license key for authentication
+ const licenseInfo = await licenseService.getLicenseInfo();
+
+ if (!licenseInfo.licenseKey) {
+ throw new Error('No license key found. Please activate a license first.');
+ }
+
+ // Create billing portal session with license key
+ const response = await licenseService.createBillingPortalSession(
+ returnUrl,
+ licenseInfo.licenseKey
+ );
+
+ // Open billing portal in new tab
+ window.open(response.url, '_blank');
+ setLoading(false);
+ } catch (error: any) {
console.error('Failed to open billing portal:', error);
alert({
alertType: 'error',
title: t('billing.portal.error', 'Failed to open billing portal'),
+ body: error.message || 'Please try again or contact support.',
});
setLoading(false);
}
diff --git a/frontend/src/proprietary/components/shared/UpgradeBanner.tsx b/frontend/src/proprietary/components/shared/UpgradeBanner.tsx
new file mode 100644
index 000000000..8a5355fe8
--- /dev/null
+++ b/frontend/src/proprietary/components/shared/UpgradeBanner.tsx
@@ -0,0 +1,144 @@
+import React, { useState, useEffect } from 'react';
+import { Group, Text, Button, ActionIcon, Paper } from '@mantine/core';
+import { useTranslation } from 'react-i18next';
+import { useAuth } from '@app/auth/UseSession';
+import { useCheckout } from '@app/contexts/CheckoutContext';
+import licenseService, { mapLicenseToTier } from '@app/services/licenseService';
+import LocalIcon from '@app/components/shared/LocalIcon';
+
+/**
+ * UpgradeBanner - Dismissable top banner encouraging users to upgrade
+ *
+ * This component demonstrates:
+ * - How to check authentication status with useAuth()
+ * - How to check license status with licenseService
+ * - How to open checkout modal with useCheckout()
+ * - How to persist dismissal state with localStorage
+ *
+ * To remove this banner:
+ * 1. Remove the import and component from AppProviders.tsx
+ * 2. Delete this file
+ */
+const UpgradeBanner: React.FC = () => {
+ const { t } = useTranslation();
+ const { user } = useAuth();
+ const { openCheckout } = useCheckout();
+ const [isVisible, setIsVisible] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Check if user should see the banner
+ useEffect(() => {
+ const checkVisibility = async () => {
+ try {
+ // Don't show if not logged in
+ if (!user) {
+ setIsVisible(false);
+ setIsLoading(false);
+ return;
+ }
+
+ // Check if banner was dismissed
+ const dismissed = localStorage.getItem('upgradeBannerDismissed');
+ if (dismissed === 'true') {
+ setIsVisible(false);
+ setIsLoading(false);
+ return;
+ }
+
+ // Check license status
+ const licenseInfo = await licenseService.getLicenseInfo();
+ const tier = mapLicenseToTier(licenseInfo);
+
+ // Show banner only for free tier users
+ if (tier === 'free' || tier === null) {
+ setIsVisible(true);
+ } else {
+ setIsVisible(false);
+ }
+ } catch (error) {
+ console.error('Error checking upgrade banner visibility:', error);
+ setIsVisible(false);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ checkVisibility();
+ }, [user]);
+
+ // Handle dismiss
+ const handleDismiss = () => {
+ localStorage.setItem('upgradeBannerDismissed', 'true');
+ setIsVisible(false);
+ };
+
+ // Handle upgrade button click
+ const handleUpgrade = () => {
+ openCheckout('server', {
+ currency: 'gbp',
+ minimumSeats: 1,
+ onSuccess: () => {
+ // Banner will auto-hide on next render when license is detected
+ setIsVisible(false);
+ },
+ });
+ };
+
+ // Don't render anything if loading or not visible
+ if (isLoading || !isVisible) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+ {t('upgradeBanner.title', 'Upgrade to Server Plan')}
+
+
+ {t('upgradeBanner.message', 'Get the most out of Stirling PDF with unlimited users and advanced features')}
+
+