mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
Feature/onboarding slides (#4955)
# Description of Changes - Added onboarding slides/upgrade banner conditions for all the following cases - 'licensed' - 'no-login-user-under-limit-no-license' - 'no-login-admin-under-limit-no-license' - 'no-login-user-over-limit-no-license' - 'no-login-admin-over-limit-no-license' - 'login-user-under-limit-no-license' - 'login-admin-under-limit-no-license' - 'login-user-over-limit-no-license' - 'login-admin-over-limit-no-license'; --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <[email protected]> Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
Connor Yoh
parent
80f2980755
commit
a8db2fda18
+57
-2
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
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';
|
||||
@@ -9,6 +9,9 @@ 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 { ManageBillingButton } from '@app/components/shared/ManageBillingButton';
|
||||
import { InfoBanner } from '@app/components/shared/InfoBanner';
|
||||
import { useLicenseAlert } from '@app/hooks/useLicenseAlert';
|
||||
import { isSupabaseConfigured } from '@app/services/supabaseClient';
|
||||
import { getPreferredCurrency, setCachedCurrency } from '@app/utils/currencyDetection';
|
||||
|
||||
@@ -25,10 +28,11 @@ const AdminPlanSection: React.FC = () => {
|
||||
const [licenseKeyInput, setLicenseKeyInput] = useState<string>('');
|
||||
const [savingLicense, setSavingLicense] = useState(false);
|
||||
const { plans, loading, error, refetch } = usePlans(currency);
|
||||
const licenseAlert = useLicenseAlert();
|
||||
|
||||
// Check if we should use static version
|
||||
useEffect(() => {
|
||||
// Check if Stripe and Supabase are configured
|
||||
// Check if Stripe is configured
|
||||
const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
||||
if (!stripeKey || !isSupabaseConfigured || error) {
|
||||
setUseStaticVersion(true);
|
||||
@@ -146,6 +150,23 @@ const AdminPlanSection: React.FC = () => {
|
||||
[openCheckout, currency, refetch, licenseInfo, t]
|
||||
);
|
||||
|
||||
const shouldShowLicenseWarning = licenseAlert.active && licenseAlert.audience === 'admin';
|
||||
const formattedUserCount = useMemo(() => {
|
||||
if (licenseAlert.totalUsers == null) {
|
||||
return t('plan.licenseWarning.overLimit', 'more than {{limit}}', {
|
||||
limit: licenseAlert.freeTierLimit,
|
||||
});
|
||||
}
|
||||
return licenseAlert.totalUsers.toLocaleString();
|
||||
}, [licenseAlert.totalUsers, licenseAlert.freeTierLimit, t]);
|
||||
|
||||
const scrollToPlans = useCallback(() => {
|
||||
const el = document.getElementById('available-plans-section');
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Show static version if Stripe is not configured or there's an error
|
||||
if (useStaticVersion) {
|
||||
return <StaticPlanSection currentLicenseInfo={licenseInfo ?? undefined} />;
|
||||
@@ -175,6 +196,40 @@ const AdminPlanSection: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
{shouldShowLicenseWarning && (
|
||||
<InfoBanner
|
||||
icon="warning-rounded"
|
||||
tone="warning"
|
||||
title={t('plan.licenseWarning.title', 'Free self-hosted limit reached')}
|
||||
message={t('plan.licenseWarning.body', {
|
||||
total: formattedUserCount,
|
||||
limit: licenseAlert.freeTierLimit,
|
||||
})}
|
||||
buttonText={t('plan.licenseWarning.cta', 'See plans')}
|
||||
buttonIcon="upgrade-rounded"
|
||||
onButtonClick={scrollToPlans}
|
||||
dismissible={false}
|
||||
minHeight={68}
|
||||
background="#FFF4E6"
|
||||
borderColor="var(--mantine-color-orange-7)"
|
||||
textColor="#9A3412"
|
||||
iconColor="#EA580C"
|
||||
buttonVariant="filled"
|
||||
buttonColor="orange.7"
|
||||
/>
|
||||
)}
|
||||
{/* Manage Subscription Button - Only show if user has active license and Supabase is configured */}
|
||||
{licenseInfo?.licenseKey && isSupabaseConfigured && (
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<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>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<AvailablePlansSection
|
||||
plans={plans}
|
||||
currentLicenseInfo={licenseInfo}
|
||||
|
||||
+10
-7
@@ -140,12 +140,15 @@ const AdminUsageSection: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const chartData = data?.endpoints?.map((e) => ({ label: e.endpoint, value: e.visits })) || [];
|
||||
const endpoints = data?.endpoints ?? [];
|
||||
const chartData = endpoints.map((e) => ({ label: e.endpoint, value: e.visits }));
|
||||
|
||||
const displayedVisits = data?.endpoints?.reduce((sum, e) => sum + e.visits, 0) || 0;
|
||||
const displayedVisits = endpoints.reduce((sum, e) => sum + e.visits, 0);
|
||||
const totalVisits = data?.totalVisits ?? displayedVisits ?? 0;
|
||||
const totalEndpoints = data?.totalEndpoints ?? endpoints.length ?? 0;
|
||||
|
||||
const displayedPercentage = (data?.totalVisits || 0) > 0
|
||||
? ((displayedVisits / (data?.totalVisits || 1)) * 100).toFixed(1)
|
||||
const displayedPercentage = totalVisits > 0
|
||||
? ((displayedVisits / (totalVisits || 1)) * 100).toFixed(1)
|
||||
: '0';
|
||||
|
||||
return (
|
||||
@@ -220,7 +223,7 @@ const AdminUsageSection: React.FC = () => {
|
||||
{t('usage.stats.totalEndpoints', 'Total Endpoints')}
|
||||
</Text>
|
||||
<Text size="lg" fw={600}>
|
||||
{data.totalEndpoints}
|
||||
{totalEndpoints}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
@@ -228,7 +231,7 @@ const AdminUsageSection: React.FC = () => {
|
||||
{t('usage.stats.totalVisits', 'Total Visits')}
|
||||
</Text>
|
||||
<Text size="lg" fw={600}>
|
||||
{data.totalVisits.toLocaleString()}
|
||||
{totalVisits.toLocaleString()}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
@@ -253,7 +256,7 @@ const AdminUsageSection: React.FC = () => {
|
||||
|
||||
{/* Chart and Table */}
|
||||
<UsageAnalyticsChart data={chartData} />
|
||||
<UsageAnalyticsTable data={data.endpoints} />
|
||||
<UsageAnalyticsTable data={endpoints} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useAppConfig } from '@app/contexts/AppConfigContext';
|
||||
import InviteMembersModal from '@app/components/shared/InviteMembersModal';
|
||||
import { useLoginRequired } from '@app/hooks/useLoginRequired';
|
||||
import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBanner';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import UpdateSeatsButton from '@app/components/shared/UpdateSeatsButton';
|
||||
import { useLicense } from '@app/contexts/LicenseContext';
|
||||
|
||||
@@ -34,6 +35,7 @@ export default function PeopleSection() {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useAppConfig();
|
||||
const { loginEnabled } = useLoginRequired();
|
||||
const navigate = useNavigate();
|
||||
const { licenseInfo: globalLicenseInfo } = useLicense();
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
@@ -53,6 +55,24 @@ export default function PeopleSection() {
|
||||
premiumEnabled: boolean;
|
||||
totalUsers: number;
|
||||
} | null>(null);
|
||||
const hasNoSlots = licenseInfo ? licenseInfo.availableSlots === 0 : false;
|
||||
const handleAddMembersClick = () => {
|
||||
if (!loginEnabled) {
|
||||
return;
|
||||
}
|
||||
if (hasNoSlots) {
|
||||
navigate('/settings/adminPlan');
|
||||
return;
|
||||
}
|
||||
setInviteModalOpened(true);
|
||||
};
|
||||
|
||||
const addMemberTooltip = !loginEnabled
|
||||
? t('workspace.people.loginRequired', 'Enable login mode first')
|
||||
: hasNoSlots
|
||||
? t('workspace.people.license.noSlotsAvailable', 'No user slots available')
|
||||
: null;
|
||||
|
||||
|
||||
// Form state for edit user modal
|
||||
const [editForm, setEditForm] = useState({
|
||||
@@ -326,9 +346,18 @@ export default function PeopleSection() {
|
||||
</Text>
|
||||
|
||||
{licenseInfo.availableSlots === 0 && (
|
||||
<Badge color="red" variant="light" size="sm">
|
||||
{t('workspace.people.license.noSlotsAvailable', 'No slots available')}
|
||||
</Badge>
|
||||
<Group gap="xs" wrap="nowrap" align="center">
|
||||
<Badge color="red" variant="light" size="sm">
|
||||
{t('workspace.people.license.noSlotsAvailable', 'No slots available')}
|
||||
</Badge>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
variant="outline"
|
||||
onClick={() => navigate('/settings/adminPlan')}
|
||||
>
|
||||
{t('workspace.people.actions.upgrade', 'Upgrade')}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{licenseInfo.grandfatheredUserCount > 0 && (
|
||||
@@ -369,14 +398,14 @@ export default function PeopleSection() {
|
||||
style={{ maxWidth: 300 }}
|
||||
/>
|
||||
<Tooltip
|
||||
label={!loginEnabled ? 'Enable login mode first' : t('workspace.people.license.noSlotsAvailable', 'No user slots available')}
|
||||
label={addMemberTooltip || undefined}
|
||||
disabled={loginEnabled && (!licenseInfo || licenseInfo.availableSlots > 0)}
|
||||
position="bottom"
|
||||
withArrow
|
||||
>
|
||||
<Button
|
||||
leftSection={<LocalIcon icon="person-add" width="1rem" height="1rem" />}
|
||||
onClick={() => setInviteModalOpened(true)}
|
||||
onClick={handleAddMembersClick}
|
||||
disabled={!loginEnabled || (licenseInfo ? licenseInfo.availableSlots === 0 : false)}
|
||||
>
|
||||
{t('workspace.people.addMembers')}
|
||||
|
||||
Reference in New Issue
Block a user