reduce bloat, change translations and change onboarding download for desktop file to show a selector button to allow you to select different OS version, if we guessed wrong

This commit is contained in:
EthanHealy01
2025-11-21 18:39:00 +00:00
parent 85b7b12125
commit 6fb44181ce
20 changed files with 205 additions and 108 deletions

View File

@@ -5062,7 +5062,7 @@
},
"welcomeSlide": {
"title": "Welcome to Stirling",
"body": "Stirling PDF is now ready for teams of all sizes.<br />This update includes a new layout, powerful new admin capabilities, and our most requested feature - <strong>Edit Text</strong>."
"body": "Stirling PDF is now ready for teams of all sizes. This update includes a new layout, powerful new admin capabilities, and our most requested feature - <strong>Edit Text</strong>."
},
"allTools": "This is the <strong>Tools</strong> panel, where you can browse and select from all available PDF tools.",
"selectCropTool": "Let's select the <strong>Crop</strong> tool to demonstrate how to use one of the tools.",
@@ -5096,6 +5096,17 @@
"overLimitBody": "Our licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. You have <strong>{{overLimitUserCopy}}</strong> Stirling users. To continue uninterrupted, upgrade to the Stirling Server plan - <strong>unlimited seats</strong>, PDF text editing, and full admin control for $99/server/mo.",
"freeBody": "Our <strong>Open-Core</strong> licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. To scale uninterrupted and get early access to our new <strong>PDF text editing tool</strong>, we recommend the Stirling Server plan - full editing and <strong>unlimited seats</strong> for $99/server/mo."
},
"desktopInstall": {
"title": "Download",
"titleWithOs": "Download for {{osLabel}}",
"body": "Stirling works best as a desktop app. You can use it offline, access documents faster, and make edits locally on your computer."
},
"planOverview": {
"adminTitle": "Admin Overview",
"userTitle": "Plan Overview",
"adminBody": "As an admin, you can manage users, configure settings, and monitor server health. The first <strong>{{freeTierLimit}}</strong> people on your server get to use Stirling free of charge.",
"userBody": "Invite teammates, assign roles, and keep your documents organized in one secure workspace. Enable login mode whenever you're ready to grow beyond solo use."
},
"securityCheck": {
"message": "The application has undergone significant changes recently. Your server admin's attention may be required. Please confirm your role to continue."
}

View File

@@ -4872,7 +4872,7 @@
},
"welcomeSlide": {
"title": "Welcome to Stirling",
"body": "Stirling PDF is now ready for teams of all sizes.<br />This update includes a new layout, powerful new admin capabilities, and our most requested feature - <strong>Edit Text</strong>."
"body": "Stirling PDF is now ready for teams of all sizes. This update includes a new layout, powerful new admin capabilities, and our most requested feature - <strong>Edit Text</strong>."
},
"allTools": "This is the <strong>Tools</strong> panel, where you can browse and select from all available PDF tools.",
"selectCropTool": "Let's select the <strong>Crop</strong> tool to demonstrate how to use one of the tools.",
@@ -4906,6 +4906,17 @@
"overLimitBody": "Our licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. You have <strong>{{overLimitUserCopy}}</strong> Stirling users. To continue uninterrupted, upgrade to the Stirling Server plan - <strong>unlimited seats</strong>, PDF text editing, and full admin control for $99/server/mo.",
"freeBody": "Our <strong>Open-Core</strong> licensing permits up to <strong>{{freeTierLimit}}</strong> users for free per server. To scale uninterrupted and get early access to our new <strong>PDF text editing tool</strong>, we recommend the Stirling Server plan - full editing and <strong>unlimited seats</strong> for $99/server/mo."
},
"desktopInstall": {
"title": "Download",
"titleWithOs": "Download for {{osLabel}}",
"body": "Stirling works best as a desktop app. You can use it offline, access documents faster, and make edits locally on your computer."
},
"planOverview": {
"adminTitle": "Admin Overview",
"userTitle": "Plan Overview",
"adminBody": "As an admin, you can manage users, configure settings, and monitor server health. The first <strong>{{freeTierLimit}}</strong> people on your server get to use Stirling free of charge.",
"userBody": "Invite teammates, assign roles, and keep your documents organized in one secure workspace. Enable login mode whenever you're ready to grow beyond solo use."
},
"securityCheck": {
"message": "The application has undergone significant changes recently. Your server admin's attention may be required. Please confirm your role to continue."
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Button, Group, ActionIcon } from '@mantine/core';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import { ButtonDefinition, type FlowState } from '@app/components/onboarding/onboardingFlowConfig';
import type { LicenseNotice } from '@app/components/onboarding/slides/types';
import type { LicenseNotice } from '@app/types/types';
import type { ButtonAction } from '@app/components/onboarding/onboardingFlowConfig';
interface RenderButtonsProps {

View File

@@ -1,4 +1,4 @@
import type { LicenseNotice } from '@app/components/onboarding/slides/types';
import type { LicenseNotice } from '@app/types/types';
export interface InitialOnboardingModalProps {
opened: boolean;

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { usePreferences } from '@app/contexts/PreferencesContext';
import { useOnboarding } from '@app/contexts/OnboardingContext';
import { useOs } from '@app/hooks/useOs';
@@ -9,7 +9,7 @@ import {
type FlowState,
type SlideId,
} from '@app/components/onboarding/onboardingFlowConfig';
import type { LicenseNotice } from '@app/components/onboarding/slides/types';
import type { LicenseNotice } from '@app/types/types';
import { resolveFlow } from './flowResolver';
import { useServerExperience } from '@app/hooks/useServerExperience';
import { DEFAULT_STATE, type InitialOnboardingModalProps, type OnboardingState } from './types';
@@ -45,6 +45,7 @@ export function useInitialOnboardingState({
} = useServerExperience();
const osType = useOs();
const navigate = useNavigate();
const selectedDownloadUrlRef = useRef<string>('');
const [state, setState] = useState<OnboardingState>(DEFAULT_STATE);
@@ -105,7 +106,7 @@ export function useInitialOnboardingState({
case 'windows':
return { label: 'Windows', url: 'https://files.stirlingpdf.com/win-installer.exe' };
case 'mac-apple':
return { label: 'Mac', url: 'https://files.stirlingpdf.com/mac-installer.dmg' };
return { label: 'Mac (Apple Silicon)', url: 'https://files.stirlingpdf.com/mac-installer.dmg' };
case 'mac-intel':
return { label: 'Mac (Intel)', url: 'https://files.stirlingpdf.com/mac-x86_64-installer.dmg' };
case 'linux-x64':
@@ -116,6 +117,16 @@ export function useInitialOnboardingState({
}
}, [osType]);
const osOptions = useMemo(() => {
const options = [
{ label: 'Windows', url: 'https://files.stirlingpdf.com/win-installer.exe', value: 'windows' },
{ label: 'Mac (Apple Silicon)', url: 'https://files.stirlingpdf.com/mac-installer.dmg', value: 'mac-apple' },
{ label: 'Mac (Intel)', url: 'https://files.stirlingpdf.com/mac-x86_64-installer.dmg', value: 'mac-intel' },
{ label: 'Linux', url: 'https://docs.stirlingpdf.com/Installation/Unix%20Installation/', value: 'linux' },
];
return options.filter(opt => opt.url);
}, []);
const { ids: flowSlideIds, type: flowType } = resolveFlow(
effectiveEnableLogin,
effectiveIsAdmin,
@@ -182,9 +193,22 @@ export function useInitialOnboardingState({
onLicenseNoticeUpdate?.(licenseNotice);
}, [licenseNotice, onLicenseNoticeUpdate]);
// Initialize ref with default URL
useEffect(() => {
if (!selectedDownloadUrlRef.current && os.url) {
selectedDownloadUrlRef.current = os.url;
}
}, [os.url]);
const handleDownloadUrlChange = useCallback((url: string) => {
selectedDownloadUrlRef.current = url;
}, []);
const currentSlide = slideDefinition.createSlide({
osLabel: os.label,
osUrl: os.url,
osOptions,
onDownloadUrlChange: handleDownloadUrlChange,
selectedRole: state.selectedRole,
onRoleSelect: handleRoleSelect,
licenseNotice,
@@ -243,7 +267,7 @@ export function useInitialOnboardingState({
closeAndMarkSeen();
return;
case 'download-selected': {
const downloadUrl = os.url || currentSlide.downloadUrl;
const downloadUrl = selectedDownloadUrlRef.current || os.url || currentSlide.downloadUrl;
if (downloadUrl) {
window.open(downloadUrl, '_blank', 'noopener');
}

View File

@@ -1,54 +0,0 @@
import { useEffect, useState } from 'react';
import apiClient from '@app/services/apiClient';
interface UseLicenseInfoOptions {
opened: boolean;
shouldFetch: boolean;
}
export function useLicenseInfo({ opened, shouldFetch }: UseLicenseInfoOptions) {
const [licenseUserCount, setLicenseUserCount] = useState<number | null>(null);
useEffect(() => {
if (!opened) {
return;
}
if (!shouldFetch) {
setLicenseUserCount(null);
return;
}
let cancelled = false;
const fetchLicenseInfo = async () => {
try {
const response = await apiClient.get<{ totalUsers?: number }>(
'/api/v1/proprietary/ui-data/admin-settings',
{
suppressErrorToast: true,
} as any,
);
if (!cancelled) {
const totalUsers = response.data?.totalUsers;
setLicenseUserCount(typeof totalUsers === 'number' ? totalUsers : null);
}
} catch (error) {
console.error('[onboarding] failed to fetch license information', error);
if (!cancelled) {
setLicenseUserCount(null);
}
}
};
fetchLicenseInfo();
return () => {
cancelled = true;
};
}, [opened, shouldFetch]);
return licenseUserCount;
}

View File

@@ -3,7 +3,7 @@ import { Modal, Button, Group, Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import AnimatedSlideBackground from '@app/components/onboarding/slides/AnimatedSlideBackground';
import ServerLicenseSlide from '@app/components/onboarding/slides/ServerLicenseSlide';
import { LicenseNotice } from '@app/components/onboarding/slides/types';
import { LicenseNotice } from '@app/types/types';
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '@app/styles/zIndex';
import styles from '@app/components/onboarding/InitialOnboardingModal/InitialOnboardingModal.module.css';

View File

@@ -4,7 +4,7 @@ import { useAppConfig } from '@app/contexts/AppConfigContext';
import { useCookieConsentContext } from '@app/contexts/CookieConsentContext';
import { useOnboarding } from '@app/contexts/OnboardingContext';
import { useAuth } from '@app/auth/UseSession';
import type { LicenseNotice } from '@app/components/onboarding/slides/types';
import type { LicenseNotice } from '@app/types/types';
import { useNavigate } from 'react-router-dom';
import {
ONBOARDING_SESSION_BLOCK_KEY,

View File

@@ -3,7 +3,7 @@ import DesktopInstallSlide from '@app/components/onboarding/slides/DesktopInstal
import SecurityCheckSlide from '@app/components/onboarding/slides/SecurityCheckSlide';
import PlanOverviewSlide from '@app/components/onboarding/slides/PlanOverviewSlide';
import ServerLicenseSlide from '@app/components/onboarding/slides/ServerLicenseSlide';
import { SlideConfig, LicenseNotice } from '@app/components/onboarding/slides/types';
import { SlideConfig, LicenseNotice } from '@app/types/types';
export type SlideId =
| 'welcome'
@@ -31,9 +31,17 @@ export interface FlowState {
selectedRole: 'admin' | 'user' | null;
}
export interface OSOption {
label: string;
url: string;
value: string;
}
export interface SlideFactoryParams {
osLabel: string;
osUrl: string;
osOptions?: OSOption[];
onDownloadUrlChange?: (url: string) => void;
selectedRole: 'admin' | 'user' | null;
onRoleSelect: (role: 'admin' | 'user' | null) => void;
licenseNotice?: LicenseNotice;
@@ -79,7 +87,7 @@ export const SLIDE_DEFINITIONS: Record<SlideId, SlideDefinition> = {
},
'desktop-install': {
id: 'desktop-install',
createSlide: ({ osLabel, osUrl }) => DesktopInstallSlide({ osLabel, osUrl }),
createSlide: ({ osLabel, osUrl, osOptions, onDownloadUrlChange }) => DesktopInstallSlide({ osLabel, osUrl, osOptions, onDownloadUrlChange }),
hero: { type: 'dual-icon' },
buttons: [
{

View File

@@ -1,6 +1,6 @@
import React from 'react';
import styles from './AnimatedSlideBackground.module.css';
import { AnimatedSlideBackgroundProps } from './types';
import { AnimatedSlideBackgroundProps } from '../../../types/types';
type CircleStyles = React.CSSProperties & {
'--circle-move-x'?: string;

View File

@@ -1,21 +1,34 @@
import React from 'react';
import { SlideConfig } from './types';
import { useTranslation } from 'react-i18next';
import { SlideConfig } from '../../../types/types';
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
import { DesktopInstallTitle, type OSOption } from './DesktopInstallTitle';
export type { OSOption };
interface DesktopInstallSlideProps {
osLabel: string;
osUrl: string;
osOptions?: OSOption[];
onDownloadUrlChange?: (url: string) => void;
}
export default function DesktopInstallSlide({ osLabel, osUrl }: DesktopInstallSlideProps): SlideConfig {
const title = osLabel ? `Download for ${osLabel}` : 'Download';
export default function DesktopInstallSlide({ osLabel, osUrl, osOptions = [], onDownloadUrlChange }: DesktopInstallSlideProps): SlideConfig {
const { t } = useTranslation();
return {
key: 'desktop-install',
title,
title: (
<DesktopInstallTitle
osLabel={osLabel}
osUrl={osUrl}
osOptions={osOptions || []}
onDownloadUrlChange={onDownloadUrlChange}
/>
),
body: (
<span>
Stirling works best as a desktop app. You can use it offline, access documents faster, and make edits locally on your computer.
{t('onboarding.desktopInstall.body', 'Stirling works best as a desktop app. You can use it offline, access documents faster, and make edits locally on your computer.')}
</span>
),
downloadUrl: osUrl,

View File

@@ -0,0 +1,93 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Menu, ActionIcon } from '@mantine/core';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
export interface OSOption {
label: string;
url: string;
value: string;
}
interface DesktopInstallTitleProps {
osLabel: string;
osUrl: string;
osOptions: OSOption[];
onDownloadUrlChange?: (url: string) => void;
}
export const DesktopInstallTitle: React.FC<DesktopInstallTitleProps> = ({
osLabel,
osUrl,
osOptions,
onDownloadUrlChange
}) => {
const { t } = useTranslation();
const [selectedOsUrl, setSelectedOsUrl] = React.useState<string>(osUrl);
React.useEffect(() => {
setSelectedOsUrl(osUrl);
}, [osUrl]);
const handleOsSelect = React.useCallback((option: OSOption) => {
setSelectedOsUrl(option.url);
onDownloadUrlChange?.(option.url);
}, [onDownloadUrlChange]);
const currentOsOption = osOptions.find(opt => opt.url === selectedOsUrl) ||
(osOptions.length > 0 ? osOptions[0] : { label: osLabel, url: osUrl });
const displayLabel = currentOsOption.label || osLabel;
const title = displayLabel
? t('onboarding.desktopInstall.titleWithOs', 'Download for {{osLabel}}', { osLabel: displayLabel })
: t('onboarding.desktopInstall.title', 'Download');
// If only one option or no options, don't show dropdown
if (osOptions.length <= 1) {
return <div style={{ textAlign: 'center', width: '100%' }}>{title}</div>;
}
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem', width: '100%' }}>
<span style={{ whiteSpace: 'nowrap' }}>{title}</span>
<Menu position="bottom" offset={5} zIndex={10000}>
<Menu.Target>
<ActionIcon
variant="transparent"
size="sm"
style={{
background: 'transparent',
border: 'none',
color: 'inherit',
padding: 0
}}
>
<ExpandMoreIcon fontSize="small" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{osOptions.map((option) => {
const isSelected = option.url === selectedOsUrl;
return (
<Menu.Item
key={option.url}
onClick={() => handleOsSelect(option)}
style={{
backgroundColor: isSelected
? 'light-dark(var(--mantine-color-blue-1), var(--mantine-color-blue-8))'
: 'transparent',
color: isSelected
? 'light-dark(var(--mantine-color-blue-9), var(--mantine-color-white))'
: 'inherit',
}}
>
{option.label}
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
</div>
);
};

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { SlideConfig, LicenseNotice } from './types';
import { useTranslation, Trans } from 'react-i18next';
import { SlideConfig, LicenseNotice } from '../../../types/types';
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
interface PlanOverviewSlideProps {
@@ -10,21 +11,28 @@ interface PlanOverviewSlideProps {
const DEFAULT_FREE_TIER_LIMIT = 5;
export default function PlanOverviewSlide({ isAdmin, licenseNotice }: PlanOverviewSlideProps): SlideConfig {
const { t } = useTranslation();
const freeTierLimit = licenseNotice?.freeTierLimit ?? DEFAULT_FREE_TIER_LIMIT;
const adminBody = (
<span>
As an admin, you can manage users, configure settings, and monitor server health. The first{' '}
<strong>{freeTierLimit}</strong> people on your server get to use Stirling free of charge.
<Trans
i18nKey="onboarding.planOverview.adminBody"
components={{ strong: <strong /> }}
values={{ freeTierLimit }}
defaults="As an admin, you can manage users, configure settings, and monitor server health. The first <strong>{{freeTierLimit}}</strong> people on your server get to use Stirling free of charge."
/>
</span>
);
return {
key: isAdmin ? 'admin-overview' : 'plan-overview',
title: isAdmin ? 'Admin Overview' : 'Plan Overview',
title: isAdmin
? t('onboarding.planOverview.adminTitle', 'Admin Overview')
: t('onboarding.planOverview.userTitle', 'Plan Overview'),
body: isAdmin ? adminBody : (
<span>
Invite teammates, assign roles, and keep your documents organized in one secure workspace. Enable login mode whenever you're ready to grow beyond solo use.
{t('onboarding.planOverview.userBody', 'Invite teammates, assign roles, and keep your documents organized in one secure workspace. Enable login mode whenever you\'re ready to grow beyond solo use.')}
</span>
),
background: {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Select } from '@mantine/core';
import styles from '../InitialOnboardingModal/InitialOnboardingModal.module.css';
import { SlideConfig } from './types';
import { SlideConfig } from '../../../types/types';
import LocalIcon from '@app/components/shared/LocalIcon';
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
import i18n from '@app/i18n';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Trans } from 'react-i18next';
import { SlideConfig, LicenseNotice } from './types';
import { SlideConfig, LicenseNotice } from '../../../types/types';
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
import i18n from '@app/i18n';

View File

@@ -1,16 +1,17 @@
import React from 'react';
import { Trans } from 'react-i18next';
import { SlideConfig } from './types';
import { useTranslation, Trans } from 'react-i18next';
import { SlideConfig } from '../../../types/types';
import styles from '../InitialOnboardingModal/InitialOnboardingModal.module.css';
import { UNIFIED_CIRCLE_CONFIG } from './unifiedBackgroundConfig';
import i18n from '@app/i18n';
export default function WelcomeSlide(): SlideConfig {
const { t } = useTranslation();
return {
key: 'welcome',
title: (
<span className={styles.welcomeTitleContainer}>
{i18n.t('onboarding.welcomeSlide.title', 'Welcome to Stirling')}
{t('onboarding.welcomeSlide.title', 'Welcome to Stirling')}
<span className={styles.v2Badge}>
V2
</span>
@@ -20,11 +21,8 @@ export default function WelcomeSlide(): SlideConfig {
<span>
<Trans
i18nKey="onboarding.welcomeSlide.body"
components={{
strong: <strong />,
br: <br />,
}}
defaults="Stirling PDF is now ready for teams of all sizes.<br />This update includes a new layout, powerful new admin capabilities, and our most requested feature - <strong>Edit Text</strong>."
components={{ strong: <strong /> }}
defaults="Stirling PDF is now ready for teams of all sizes. This update includes a new layout, powerful new admin capabilities, and our most requested feature - <strong>Edit Text</strong>."
/>
</span>
),

View File

@@ -1,4 +1,4 @@
import { AnimatedCircleConfig } from './types';
import { AnimatedCircleConfig } from '../../../types/types';
/**
* Unified circle background configuration used across all onboarding slides.

View File

@@ -270,12 +270,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
<div
key={buttonConfig.id}
data-tour="help-button"
onClick={() =>
startTour('tools', {
source: 'quick-access-help-button',
metadata: { entry: 'direct' },
})
}
onClick={() => startTour('tools')}
>
{renderNavButton(buttonConfig, index)}
</div>
@@ -292,12 +287,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
<Menu.Dropdown>
<Menu.Item
leftSection={<LocalIcon icon="view-carousel-rounded" width="1.25rem" height="1.25rem" />}
onClick={() =>
startTour('tools', {
source: 'quick-access-help-menu',
metadata: { entry: 'menu-tools' },
})
}
onClick={() => startTour('tools')}
>
<div>
<div style={{ fontWeight: 500 }}>
@@ -310,12 +300,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
</Menu.Item>
<Menu.Item
leftSection={<LocalIcon icon="admin-panel-settings-rounded" width="1.25rem" height="1.25rem" />}
onClick={() =>
startTour('admin', {
source: 'quick-access-help-menu',
metadata: { entry: 'menu-admin' },
})
}
onClick={() => startTour('admin')}
>
<div>
<div style={{ fontWeight: 500 }}>

View File

@@ -1,4 +1,4 @@
import type { LicenseNotice } from '@app/components/onboarding/slides/types';
import type { LicenseNotice } from '@app/types/types';
export const ONBOARDING_SESSION_BLOCK_KEY = 'stirling-onboarding-session-active';
export const ONBOARDING_SESSION_EVENT = 'stirling:onboarding-session-started';