Manage button on cards

This commit is contained in:
Connor Yoh
2025-11-24 11:58:02 +00:00
parent ffdff7a03b
commit 88ebd03ba0
4 changed files with 76 additions and 52 deletions
@@ -1,5 +1,5 @@
import React, { useState, useCallback, useEffect } from 'react';
import { Divider, Loader, Alert, Select, Group, Text, Collapse, Button, TextInput, Stack, Paper } from '@mantine/core';
import { Divider, Loader, Alert, Group, Text, Collapse, Button, TextInput, Stack, Paper } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { usePlans } from '@app/hooks/usePlans';
import licenseService, { PlanTierGroup, mapLicenseToTier } from '@app/services/licenseService';
@@ -9,8 +9,6 @@ import AvailablePlansSection from '@app/components/shared/config/configSections/
import StaticPlanSection from '@app/components/shared/config/configSections/plan/StaticPlanSection';
import { alert } from '@app/components/toast';
import LocalIcon from '@app/components/shared/LocalIcon';
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
import { ManageBillingButton } from '@app/components/shared/ManageBillingButton';
import { isSupabaseConfigured } from '@app/services/supabaseClient';
const AdminPlanSection: React.FC = () => {
@@ -80,6 +78,30 @@ const AdminPlanSection: React.FC = () => {
{ value: 'idr', label: 'Indonesian rupiah (IDR, Rp)' },
];
const handleManageClick = useCallback(async () => {
try {
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(
window.location.href,
licenseInfo.licenseKey
);
// Open billing portal in new tab
window.open(response.url, '_blank');
} 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.',
});
}
}, [licenseInfo, t]);
const handleUpgradeClick = useCallback(
(planGroup: PlanTierGroup) => {
// Only allow upgrades for server and enterprise tiers
@@ -143,40 +165,14 @@ const AdminPlanSection: React.FC = () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
{/* Currency Selection & Manage Subscription */}
<Paper withBorder p="md" radius="md">
<Stack gap="md">
<Group justify="space-between" align="center">
<Text size="lg" fw={600}>
{t('plan.currency', 'Currency')}
</Text>
<Select
value={currency}
onChange={(value) => setCurrency(value || 'gbp')}
data={currencyOptions}
searchable
clearable={false}
w={300}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
/>
</Group>
{/* Manage Subscription Button - Only show if user has active license and Supabase is configured */}
{licenseInfo?.licenseKey && isSupabaseConfigured && (
<Group justify="space-between" align="center">
<Text size="sm" c="dimmed">
{t('plan.manageSubscription.description', 'Manage your subscription, billing, and payment methods')}
</Text>
<ManageBillingButton />
</Group>
)}
</Stack>
</Paper>
<AvailablePlansSection
plans={plans}
currentLicenseInfo={licenseInfo}
onUpgradeClick={handleUpgradeClick}
onManageClick={handleManageClick}
currency={currency}
onCurrencyChange={setCurrency}
currencyOptions={currencyOptions}
/>
<Divider />
@@ -1,21 +1,30 @@
import React, { useState, useMemo } from 'react';
import { Button, Collapse } from '@mantine/core';
import { Button, Collapse, Select, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import licenseService, { PlanTier, PlanTierGroup, LicenseInfo, mapLicenseToTier } from '@app/services/licenseService';
import PlanCard from '@app/components/shared/config/configSections/plan/PlanCard';
import FeatureComparisonTable from '@app/components/shared/config/configSections/plan/FeatureComparisonTable';
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
interface AvailablePlansSectionProps {
plans: PlanTier[];
currentPlanId?: string;
currentLicenseInfo?: LicenseInfo | null;
onUpgradeClick: (planGroup: PlanTierGroup) => void;
onManageClick?: () => void;
currency?: string;
onCurrencyChange?: (currency: string) => void;
currencyOptions?: { value: string; label: string }[];
}
const AvailablePlansSection: React.FC<AvailablePlansSectionProps> = ({
plans,
currentLicenseInfo,
onUpgradeClick,
onManageClick,
currency,
onCurrencyChange,
currencyOptions,
}) => {
const { t } = useTranslation();
const [showComparison, setShowComparison] = useState(false);
@@ -58,18 +67,33 @@ const AvailablePlansSection: React.FC<AvailablePlansSectionProps> = ({
return (
<div>
<h3 style={{ margin: 0, color: 'var(--mantine-color-text)', fontSize: '1rem' }}>
{t('plan.availablePlans.title', 'Available Plans')}
</h3>
<p
style={{
margin: '0.25rem 0 1rem 0',
color: 'var(--mantine-color-dimmed)',
fontSize: '0.875rem',
}}
>
{t('plan.availablePlans.subtitle', 'Choose the plan that fits your needs')}
</p>
<Group justify="space-between" align="flex-start" mb="xs">
<div>
<h3 style={{ margin: 0, color: 'var(--mantine-color-text)', fontSize: '1rem' }}>
{t('plan.availablePlans.title', 'Available Plans')}
</h3>
<p
style={{
margin: '0.25rem 0 0 0',
color: 'var(--mantine-color-dimmed)',
fontSize: '0.875rem',
}}
>
{t('plan.availablePlans.subtitle', 'Choose the plan that fits your needs')}
</p>
</div>
{currency && onCurrencyChange && currencyOptions && (
<Select
value={currency}
onChange={(value) => onCurrencyChange(value || 'gbp')}
data={currencyOptions}
searchable
clearable={false}
w={300}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_OVER_CONFIG_MODAL }}
/>
)}
</Group>
<div
style={{
@@ -88,6 +112,7 @@ const AvailablePlansSection: React.FC<AvailablePlansSectionProps> = ({
currentLicenseInfo={currentLicenseInfo}
currentTier={currentTier}
onUpgradeClick={onUpgradeClick}
onManageClick={onManageClick}
/>
))}
</div>
@@ -14,9 +14,10 @@ interface PlanCardProps {
currentLicenseInfo?: LicenseInfo | null;
currentTier?: 'free' | 'server' | 'enterprise' | null;
onUpgradeClick: (planGroup: PlanTierGroup) => void;
onManageClick?: () => void;
}
const PlanCard: React.FC<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngrade, currentLicenseInfo, currentTier, onUpgradeClick }) => {
const PlanCard: React.FC<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngrade, currentLicenseInfo, currentTier, onUpgradeClick, onManageClick }) => {
const { t } = useTranslation();
// Render Free plan
@@ -160,15 +161,15 @@ const PlanCard: React.FC<PlanCardProps> = ({ planGroup, isCurrentTier, isDowngra
withArrow
>
<Button
variant={isCurrentTier || isDowngrade || isEnterpriseBlockedForFree ? 'light' : 'filled'}
variant={isCurrentTier ? 'filled' : isDowngrade ? 'filled' : isEnterpriseBlockedForFree ? 'light' : 'filled'}
fullWidth
onClick={() => onUpgradeClick(planGroup)}
disabled={isCurrentTier || isDowngrade || isEnterpriseBlockedForFree}
onClick={() => isCurrentTier && onManageClick ? onManageClick() : onUpgradeClick(planGroup)}
disabled={isDowngrade || isEnterpriseBlockedForFree}
>
{isCurrentTier
? t('plan.current', 'Current Plan')
? t('plan.manage', 'Manage')
: isDowngrade
? t('plan.includedInCurrent', 'Included in Your Plan')
? t('plan.free.included', 'Included')
: isEnterpriseBlockedForFree
? t('plan.enterprise.requiresServer', 'Requires Server')
: isEnterprise
@@ -74,12 +74,14 @@ export const PLAN_HIGHLIGHTS = {
'Self-hosted on your infrastructure',
'Unlimited users',
'Advanced integrations',
'Editing text in PDFs',
'Cancel anytime'
],
SERVER_YEARLY: [
'Self-hosted on your infrastructure',
'Unlimited users',
'Advanced integrations',
'Editing text in PDFs',
'Save with annual billing'
],
ENTERPRISE_MONTHLY: [