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:
EthanHealy01
2025-11-25 13:45:02 +00:00
committed by GitHub
co-authored by Anthony Stirling Connor Yoh
parent 80f2980755
commit a8db2fda18
66 changed files with 5641 additions and 1248 deletions
@@ -1,5 +1,6 @@
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import apiClient from '@app/services/apiClient';
import { getSimulatedAppConfig } from '@app/testing/serverExperienceSimulations';
/**
* Sleep utility for delays
@@ -44,6 +45,8 @@ export interface AppConfig {
activeSecurity?: boolean;
dependenciesReady?: boolean;
error?: string;
isNewServer?: boolean;
isNewUser?: boolean;
}
export type AppConfigBootstrapMode = 'blocking' | 'non-blocking';
@@ -106,6 +109,15 @@ export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const testConfig = getSimulatedAppConfig();
if (testConfig) {
setConfig(testConfig);
setFetchCount((prev) => prev + 1);
setHasResolvedConfig(true);
setLoading(false);
return;
}
if (attempt > 0) {
const delay = initialDelay * Math.pow(2, attempt - 1);
console.log(`[AppConfig] Retry attempt ${attempt}/${maxRetries} after ${delay}ms delay...`);
@@ -0,0 +1,45 @@
import React, { createContext, useContext, useMemo } from 'react';
import { useCookieConsent } from '@app/hooks/useCookieConsent';
import { useAppConfig } from '@app/contexts/AppConfigContext';
interface CookieConsentContextValue {
isReady: boolean;
showCookieConsent: () => void;
showCookiePreferences: () => void;
hasResponded: boolean;
}
const CookieConsentContext = createContext<CookieConsentContextValue | undefined>(undefined);
export const CookieConsentProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { config } = useAppConfig();
const analyticsEnabled = config ? config.enableAnalytics !== false : false;
const {
showCookieConsent,
showCookiePreferences,
isInitialized,
hasResponded,
} = useCookieConsent({ analyticsEnabled });
const value = useMemo<CookieConsentContextValue>(() => ({
isReady: analyticsEnabled && isInitialized,
showCookieConsent,
showCookiePreferences,
hasResponded,
}), [analyticsEnabled, hasResponded, isInitialized, showCookieConsent, showCookiePreferences]);
return (
<CookieConsentContext.Provider value={value}>
{children}
</CookieConsentContext.Provider>
);
};
export const useCookieConsentContext = (): CookieConsentContextValue => {
const context = useContext(CookieConsentContext);
if (!context) {
throw new Error('useCookieConsentContext must be used within a CookieConsentProvider');
}
return context;
};
@@ -1,45 +1,107 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import React, { createContext, useContext, useState, useCallback } from 'react';
import { usePreferences } from '@app/contexts/PreferencesContext';
import { useShouldShowWelcomeModal } from '@app/hooks/useShouldShowWelcomeModal';
export type TourType = 'tools' | 'admin';
export interface StartTourOptions {
source?: string;
skipToolPromptRequirement?: boolean;
metadata?: Record<string, unknown>;
}
interface PendingTourRequest {
type: TourType;
source?: string;
metadata?: Record<string, unknown>;
requestedAt: number;
}
interface OnboardingContextValue {
isOpen: boolean;
currentStep: number;
tourType: TourType;
setCurrentStep: (step: number) => void;
startTour: (type?: TourType) => void;
startTour: (type?: TourType, options?: StartTourOptions) => void;
closeTour: () => void;
completeTour: () => void;
resetTour: (type?: TourType) => void;
showWelcomeModal: boolean;
setShowWelcomeModal: (show: boolean) => void;
startAfterToolModeSelection: boolean;
setStartAfterToolModeSelection: (value: boolean) => void;
pendingTourRequest: PendingTourRequest | null;
clearPendingTourRequest: () => void;
}
const OnboardingContext = createContext<OnboardingContextValue | undefined>(undefined);
export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { updatePreference } = usePreferences();
const { preferences, updatePreference } = usePreferences();
const [isOpen, setIsOpen] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [tourType, setTourType] = useState<TourType>('tools');
const [showWelcomeModal, setShowWelcomeModal] = useState(false);
const shouldShow = useShouldShowWelcomeModal();
const [startAfterToolModeSelection, setStartAfterToolModeSelection] = useState(false);
const [pendingTourRequest, setPendingTourRequest] = useState<PendingTourRequest | null>(null);
// Auto-show welcome modal for first-time users
useEffect(() => {
if (shouldShow) {
setShowWelcomeModal(true);
}
}, [shouldShow]);
const startTour = useCallback((type: TourType = 'tools') => {
const openTour = useCallback((type: TourType = 'tools') => {
setTourType(type);
setCurrentStep(0);
setIsOpen(true);
}, []);
const startTour = useCallback(
(type: TourType = 'tools', options?: StartTourOptions) => {
const requestedType = type ?? 'tools';
const source = options?.source ?? 'unspecified';
const metadata = options?.metadata;
const skipToolPromptRequirement = options?.skipToolPromptRequirement ?? false;
const toolPromptSeen = preferences.toolPanelModePromptSeen;
const hasSelectedToolPanelMode = preferences.hasSelectedToolPanelMode;
const hasToolPreference = toolPromptSeen || hasSelectedToolPanelMode;
const shouldDefer = !skipToolPromptRequirement && !hasToolPreference;
console.log('[onboarding] startTour invoked', {
requestedType,
source,
toolPromptSeen,
hasSelectedToolPanelMode,
shouldDefer,
hasPendingTourRequest: !!pendingTourRequest,
metadata,
});
if (shouldDefer) {
setPendingTourRequest({
type: requestedType,
source,
metadata,
requestedAt: Date.now(),
});
setStartAfterToolModeSelection(true);
console.log('[onboarding] deferring tour launch until tool panel mode selection completes', {
requestedType,
source,
});
return;
}
if (pendingTourRequest) {
console.log('[onboarding] clearing previous pending tour request before starting new tour', {
previousRequest: pendingTourRequest,
newType: requestedType,
source,
});
}
setPendingTourRequest(null);
setStartAfterToolModeSelection(false);
console.log('[onboarding] starting tour', {
requestedType,
source,
});
openTour(requestedType);
},
[openTour, pendingTourRequest, preferences.toolPanelModePromptSeen, preferences.hasSelectedToolPanelMode],
);
const closeTour = useCallback(() => {
setIsOpen(false);
}, []);
@@ -56,6 +118,16 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
setIsOpen(true);
}, [updatePreference]);
const clearPendingTourRequest = useCallback(() => {
if (pendingTourRequest) {
console.log('[onboarding] clearing pending tour request manually', {
pendingTourRequest,
});
}
setPendingTourRequest(null);
setStartAfterToolModeSelection(false);
}, [pendingTourRequest]);
return (
<OnboardingContext.Provider
value={{
@@ -67,8 +139,10 @@ export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ ch
closeTour,
completeTour,
resetTour,
showWelcomeModal,
setShowWelcomeModal,
startAfterToolModeSelection,
setStartAfterToolModeSelection,
pendingTourRequest,
clearPendingTourRequest,
}}
>
{children}
@@ -146,6 +146,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
const setToolPanelMode = useCallback((mode: ToolPanelMode) => {
dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: mode });
updatePreference('defaultToolPanelMode', mode);
updatePreference('hasSelectedToolPanelMode', true);
}, [updatePreference]);