Skip to content

Commit 2f149c3

Browse files
committed
fix(mobile): prevent master password screen freeze during user transitions
- Add synchronous userId change detection using useRef to catch sign-in immediately - Introduce userChanging state flag to block app rendering during user transitions - Prevent notes screen from mounting and attempting decryption before master password entry - Reset master password state on userId change to ensure clean state
1 parent 077fc41 commit 2f149c3

File tree

3 files changed

+43
-29
lines changed

3 files changed

+43
-29
lines changed

apps/mobile/v1/src/components/AppWrapper.tsx

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useRef } from 'react';
22
import { View, ActivityIndicator } from 'react-native';
33
import { useAuth, useUser } from '@clerk/clerk-expo';
44
import { useTheme } from '../theme';
@@ -24,8 +24,27 @@ export const AppWrapper: React.FC<AppWrapperProps> = ({ children }) => {
2424
} = useMasterPassword();
2525

2626
const [showLoading, setShowLoading] = useState(false);
27+
const lastUserIdRef = useRef<string | undefined>(undefined);
28+
const [userChanging, setUserChanging] = useState(false);
2729

28-
const isLoading = !isLoaded || (isSignedIn && !userLoaded) || (isSignedIn && userLoaded && !user) || (isSignedIn && user && isChecking);
30+
// Detect userId change SYNCHRONOUSLY in render
31+
if (userId !== lastUserIdRef.current) {
32+
lastUserIdRef.current = userId;
33+
if (userId && !userChanging) {
34+
// User just signed in - block app from rendering
35+
setUserChanging(true);
36+
}
37+
}
38+
39+
// Clear userChanging flag once master password state is determined
40+
useEffect(() => {
41+
if (userChanging && !isChecking) {
42+
setUserChanging(false);
43+
}
44+
}, [userChanging, isChecking]);
45+
46+
// Simple logic: show loading only for initial Clerk loading, nothing else
47+
const isLoading = !isLoaded || (isSignedIn && !userLoaded) || (isSignedIn && userLoaded && !user);
2948

3049
// Handle loading delay
3150
useEffect(() => {
@@ -62,19 +81,6 @@ export const AppWrapper: React.FC<AppWrapperProps> = ({ children }) => {
6281
}
6382
}, [isSignedIn, user?.id, user?.primaryEmailAddress?.emailAddress, user?.username]);
6483

65-
if (__DEV__) {
66-
console.log('AppWrapper Auth status:', {
67-
isSignedIn,
68-
isLoaded,
69-
userLoaded,
70-
userId: user?.id,
71-
hasSession: !!user,
72-
needsUnlock,
73-
isNewSetup,
74-
isChecking,
75-
});
76-
}
77-
7884
// Show loading while Clerk initializes, user loads, or checking master password
7985
if (showLoading) {
8086
return (
@@ -104,8 +110,10 @@ export const AppWrapper: React.FC<AppWrapperProps> = ({ children }) => {
104110
return <AuthScreen />;
105111
}
106112

107-
// User is signed in - show master password screen or main app
108-
if (needsUnlock) {
113+
// User is signed in - check master password state
114+
// IMPORTANT: If we're checking OR need unlock OR user just changed, show master password screen
115+
// This prevents notes from loading prematurely
116+
if (needsUnlock || isChecking || userChanging) {
109117
return (
110118
<MasterPasswordScreen
111119
key={userId} // Force remount when userId changes to reset all state
@@ -116,6 +124,6 @@ export const AppWrapper: React.FC<AppWrapperProps> = ({ children }) => {
116124
);
117125
}
118126

119-
// Show main app
127+
// Only render main app when explicitly unlocked and stable
120128
return <>{children}</>;
121129
};

apps/mobile/v1/src/components/MasterPasswordDialog/index.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,12 @@ export function MasterPasswordScreen({
2626
const [isLoading, setIsLoading] = useState(false);
2727
const animatedValue = useKeyboardHandler();
2828

29-
useEffect(() => {
30-
// Vibrate when component mounts
31-
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
32-
}, []);
33-
34-
// Reset loading state when component mounts or isNewSetup changes
29+
// Reset loading state and vibrate when component mounts
3530
// This ensures the form is shown after sign out/sign in
3631
useEffect(() => {
3732
setIsLoading(false);
38-
}, [isNewSetup]);
33+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
34+
}, []);
3935

4036
const handleFormSubmit = async (password: string) => {
4137
setIsLoading(true);
@@ -70,6 +66,7 @@ export function MasterPasswordScreen({
7066
>
7167
{!isLoading ? (
7268
<MasterPasswordForm
69+
key={`password-form-${isNewSetup}`} // Force remount when setup state changes
7370
isNewSetup={isNewSetup}
7471
onSubmit={handleFormSubmit}
7572
/>

apps/mobile/v1/src/hooks/useMasterPassword.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,33 @@ export function forceGlobalMasterPasswordRefresh() {
1818
export function useMasterPassword() {
1919
const { user, isLoaded: userLoaded } = useUser();
2020
const { isSignedIn } = useAuth();
21-
const [needsUnlock, setNeedsUnlock] = useState(false);
21+
const [needsUnlock, setNeedsUnlock] = useState(true); // Default to true to prevent premature loading
2222
const [isNewSetup, setIsNewSetup] = useState(false);
2323
const [isChecking, setIsChecking] = useState(true);
2424
const [, setLastCheckTime] = useState<number>(0);
2525

2626
const userId = user?.id;
2727

28+
// Reset state when user changes (sign out/sign in)
29+
useEffect(() => {
30+
if (userId) {
31+
// New user signed in - reset to locked state
32+
setNeedsUnlock(true);
33+
setIsChecking(true);
34+
setIsNewSetup(false);
35+
}
36+
}, [userId]);
37+
2838
const checkMasterPasswordStatus = useCallback(async () => {
2939
if (!userLoaded || !userId || !isSignedIn) {
3040
setIsChecking(true);
41+
setNeedsUnlock(true); // Assume locked when no user
3142
return;
3243
}
3344

3445
try {
3546
setIsChecking(true);
47+
setNeedsUnlock(true); // Assume locked while checking
3648

3749
// Check if user has a master password set up
3850
const hasPassword = await hasMasterPassword(userId);
@@ -53,9 +65,6 @@ export function useMasterPassword() {
5365
// Already unlocked
5466
setIsNewSetup(false);
5567
setNeedsUnlock(false);
56-
if (__DEV__) {
57-
console.log('✅ Master password already unlocked');
58-
}
5968
}
6069
}
6170

0 commit comments

Comments
 (0)