Integrate offline support and improve error handling across app

- Add `OfflineContext` to manage offline state and interactions within components.
- Refactor session logic in `ScribeControllerBar` and `page.tsx` to handle offline scenarios (e.g., check connectivity before enabling GPT features).
- Enhance offline PIN setup and verification with better flow and error messaging.
- Optimize database IPC handlers to initialize and sync data in offline mode.
- Refactor code to clean up redundant logs and ensure stricter typings.
- Improve consistency and structure in handling online and offline operations for smoother user experience.
This commit is contained in:
natreex
2025-11-26 15:25:53 -05:00
parent 5cceceaea9
commit ac95e00127
10 changed files with 95 additions and 159 deletions

View File

@@ -14,7 +14,6 @@ import {SessionContext} from '@/context/SessionContext';
import {SessionProps} from "@/lib/models/Session";
import User, {UserProps} from "@/lib/models/User";
import {BookProps} from "@/lib/models/Book";
// Removed Next.js router imports for Electron
import ScribeTopBar from "@/components/ScribeTopBar";
import ScribeControllerBar from "@/components/ScribeControllerBar";
import ScribeLeftBar from "@/components/leftbar/ScribeLeftBar";
@@ -27,7 +26,6 @@ import {faBookMedical, faFeather} from "@fortawesome/free-solid-svg-icons";
import TermsOfUse from "@/components/TermsOfUse";
import frMessages from '@/lib/locales/fr.json';
import enMessages from '@/lib/locales/en.json';
// Removed Next.js Image import for Electron
import {NextIntlClientProvider, useTranslations} from "next-intl";
import {LangContext} from "@/context/LangContext";
import {AIUsageContext} from "@/context/AIUsageContext";
@@ -45,7 +43,7 @@ function ScribeContent() {
const t = useTranslations();
const {lang: locale} = useContext(LangContext);
const {errorMessage} = useContext(AlertContext);
const {initializeDatabase} = useContext(OfflineContext);
const {initializeDatabase, setOfflineMode, isCurrentlyOffline} = useContext(OfflineContext);
const editor: Editor | null = useEditor({
extensions: [
StarterKit,
@@ -57,8 +55,7 @@ function ScribeContent() {
injectCSS: false,
immediatelyRender: false,
});
// Router removed for Electron - using window.location instead
const [session, setSession] = useState<SessionProps>({user: null, accessToken: '', isConnected: false});
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
@@ -68,8 +65,6 @@ function ScribeContent() {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [sessionAttempts, setSessionAttempts] = useState<number>(0)
const [isTermsAccepted, setIsTermsAccepted] = useState<boolean>(false);
const [homeStepsGuide, setHomeStepsGuide] = useState<boolean>(false);
const [showPinSetup, setShowPinSetup] = useState<boolean>(false);
@@ -151,12 +146,7 @@ function ScribeContent() {
setIsTermsAccepted(session.user?.termsAccepted ?? false);
setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic'));
setIsLoading(false);
} else {
if (sessionAttempts > 2) {
// Redirect handled by checkAuthentification
}
}
setSessionAttempts(sessionAttempts + 1);
}, [session]);
useEffect((): void => {
@@ -166,54 +156,50 @@ function ScribeContent() {
}, [currentBook]);
// Check for PIN setup after successful connection
useEffect(() => {
useEffect(():void => {
async function checkPinSetup() {
if (session.isConnected && window.electron) {
try {
const offlineStatus = await window.electron.offlineModeGet();
console.log('[Page] Session connected, offline status:', offlineStatus);
if (!offlineStatus.hasPin) {
console.log('[Page] No PIN configured, will show setup dialog');
// Show PIN setup dialog after a short delay
setTimeout(() => {
setTimeout(():void => {
console.log('[Page] Showing PIN setup dialog');
setShowPinSetup(true);
}, 2000); // 2 seconds delay after page load
}, 2000);
}
} catch (e:unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage('Unknown error occurred while checking offline mode')
}
} catch (error) {
console.error('[Page] Error checking offline mode:', error);
}
}
}
checkPinSetup();
}, [session.isConnected]); // Run when session connection status changes
checkPinSetup().then();
}, [session.isConnected]);
async function handlePinVerifySuccess(userId: string): Promise<void> {
console.log('[OfflinePin] PIN verified successfully for user:', userId);
try {
// Initialize database with user's encryption key
if (window.electron) {
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
if (encryptionKey) {
await window.electron.dbInitialize(userId, encryptionKey);
// Load user from local DB
const localUser = await window.electron.invoke('db:user:info');
if (localUser && localUser.success) {
// Use local data and continue in offline mode
const localUser:UserProps = await window.electron.invoke('db:user:info');
if (localUser && localUser.id) {
setSession({
isConnected: true,
user: localUser.data,
user: localUser,
accessToken: 'offline', // Special offline token
});
setShowPinVerify(false);
setCurrentCredits(localUser.data.creditsBalance || 0);
setAmountSpent(localUser.data.aiUsage || 0);
console.log('[OfflinePin] Running in offline mode');
setCurrentCredits(localUser.creditsBalance || 0);
setAmountSpent(localUser.aiUsage || 0);
} else {
errorMessage(t("homePage.errors.localDataError"));
if (window.electron) {
@@ -282,29 +268,18 @@ function ScribeContent() {
return;
}
console.log('user: ' , user);
// Initialize user in Electron (sets userId and creates/gets encryption key)
if (window.electron && user.id) {
try {
const initResult = await window.electron.initUser(user.id);
if (!initResult.success) {
console.error('[Page] Failed to initialize user:', initResult.error);
} else {
console.log('[Page] User initialized successfully, key created:', initResult.keyCreated);
// Check if PIN is configured for offline mode
try {
const offlineStatus = await window.electron.offlineModeGet();
console.log('[Page] Offline status:', offlineStatus);
if (!offlineStatus.hasPin) {
// First login or no PIN configured yet
// Show PIN setup dialog after a short delay
console.log('[Page] No PIN configured, will show setup dialog');
setTimeout(() => {
console.log('[Page] Showing PIN setup dialog');
setTimeout(():void => {
setShowPinSetup(true);
}, 2000); // 2 seconds delay after successful login
}, 2000);
}
} catch (error) {
console.error('[Page] Error checking offline mode:', error);
@@ -321,15 +296,10 @@ function ScribeContent() {
});
setCurrentCredits(user.creditsBalance)
setAmountSpent(user.aiUsage)
// Initialiser la DB locale en Electron
if (window.electron && user.id) {
try {
const dbInitialized = await initializeDatabase(user.id);
const dbInitialized:boolean = await initializeDatabase(user.id);
if (dbInitialized) {
console.log('Database initialized successfully');
// Sync user to local DB (only if not exists)
try {
await window.electron.invoke('db:user:sync', {
userId: user.id,
@@ -341,7 +311,6 @@ function ScribeContent() {
console.log('User synced to local DB');
} catch (syncError) {
console.error('Failed to sync user to local DB:', syncError);
// Non-blocking error, continue anyway
}
}
} catch (error) {
@@ -349,13 +318,12 @@ function ScribeContent() {
}
}
} catch (e: unknown) {
console.log('[Auth] Server error, checking offline mode...');
if (window.electron) {
try {
const offlineStatus = await window.electron.offlineModeGet();
if (offlineStatus.hasPin && offlineStatus.lastUserId) {
console.log('[Auth] Server unreachable but PIN configured, showing PIN verification');
setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false}));
setShowPinVerify(true);
setIsLoading(false);
return;
@@ -369,8 +337,7 @@ function ScribeContent() {
console.error('[Auth] Error checking offline mode:', offlineError);
}
}
// If not in offline mode or no PIN configured, show error and logout
if (e instanceof Error) {
errorMessage(e.message);
} else {
@@ -383,7 +350,7 @@ function ScribeContent() {
const offlineStatus = await window.electron.offlineModeGet();
if (offlineStatus.hasPin && offlineStatus.lastUserId) {
console.log('[Auth] No token but PIN configured, showing PIN verification for offline mode');
setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false}));
setShowPinVerify(true);
setIsLoading(false);
return;
@@ -428,7 +395,12 @@ function ScribeContent() {
async function getLastChapter(): Promise<void> {
if (session?.accessToken) {
try {
const response: ChapterProps | null = await System.authGetQueryToServer<ChapterProps | null>(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
let response: ChapterProps | null
if (isCurrentlyOffline()){
response = await window.electron.invoke('db:chapter:last', currentBook?.bookId)
} else {
response = await System.authGetQueryToServer<ChapterProps | null>(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
}
if (response) {
setCurrentChapter(response)
} else {
@@ -489,12 +461,12 @@ function ScribeContent() {
</EditorContext.Provider>
</div>
{
homeStepsGuide &&
homeStepsGuide && !isCurrentlyOffline() &&
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
onClose={(): void => setHomeStepsGuide(false)}/>
}
{
!isTermsAccepted && <TermsOfUse onAccept={handleTermsAcceptance}/>
!isTermsAccepted && !isCurrentlyOffline() && <TermsOfUse onAccept={handleTermsAcceptance}/>
}
{
showPinSetup && window.electron && (