mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 02:54:06 +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
@@ -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]);
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user