@umituz/react-native-auth 2.7.7 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/presentation/hooks/useAuth.ts +107 -22
- package/src/presentation/screens/AccountScreen.tsx +4 -5
- package/src/presentation/stores/authStore.ts +220 -0
- package/src/presentation/hooks/useAuthActions.ts +0 -92
- package/src/presentation/hooks/useAuthState.ts +0 -131
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Authentication service for React Native apps - Secure, type-safe, and production-ready. Provider-agnostic design with dependency injection, configurable validation, and comprehensive error handling.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -178,6 +178,13 @@ export type { AccountActionsConfig, AccountActionsProps } from './presentation/c
|
|
|
178
178
|
export { useAuthModalStore } from './presentation/stores/authModalStore';
|
|
179
179
|
export type { AuthModalMode } from './presentation/stores/authModalStore';
|
|
180
180
|
|
|
181
|
+
export {
|
|
182
|
+
useAuthStore,
|
|
183
|
+
initializeAuthListener,
|
|
184
|
+
resetAuthListener,
|
|
185
|
+
selectIsAuthenticated,
|
|
186
|
+
} from './presentation/stores/authStore';
|
|
187
|
+
|
|
181
188
|
// =============================================================================
|
|
182
189
|
// PRESENTATION LAYER - Utilities
|
|
183
190
|
// =============================================================================
|
|
@@ -2,12 +2,23 @@
|
|
|
2
2
|
* useAuth Hook
|
|
3
3
|
* React hook for authentication state management
|
|
4
4
|
*
|
|
5
|
-
* Uses
|
|
6
|
-
*
|
|
5
|
+
* Uses centralized Zustand store for auth state.
|
|
6
|
+
* Single source of truth - no duplicate subscriptions.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const { user, isAuthenticated, signIn, signUp, signOut } = useAuth();
|
|
11
|
+
* ```
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
14
|
+
import { useCallback } from "react";
|
|
15
|
+
import { useAuthStore, selectIsAuthenticated } from "../stores/authStore";
|
|
16
|
+
import {
|
|
17
|
+
useSignInMutation,
|
|
18
|
+
useSignUpMutation,
|
|
19
|
+
useSignOutMutation,
|
|
20
|
+
useGuestModeMutation,
|
|
21
|
+
} from "./mutations/useAuthMutations";
|
|
11
22
|
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
12
23
|
|
|
13
24
|
export interface UseAuthResult {
|
|
@@ -35,31 +46,105 @@ export interface UseAuthResult {
|
|
|
35
46
|
|
|
36
47
|
/**
|
|
37
48
|
* Hook for authentication state management
|
|
38
|
-
*
|
|
39
|
-
* Uses
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* - Loading states
|
|
43
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* Uses centralized Zustand store - all components share the same state.
|
|
51
|
+
* Must call initializeAuthListener() once in app root.
|
|
52
|
+
*
|
|
44
53
|
* @example
|
|
45
54
|
* ```typescript
|
|
46
55
|
* const { user, isAuthenticated, signIn, signUp, signOut } = useAuth();
|
|
47
56
|
* ```
|
|
48
57
|
*/
|
|
49
58
|
export function useAuth(): UseAuthResult {
|
|
50
|
-
|
|
51
|
-
const
|
|
59
|
+
// State from store
|
|
60
|
+
const user = useAuthStore((state) => state.user);
|
|
61
|
+
const loading = useAuthStore((state) => state.loading);
|
|
62
|
+
const isGuest = useAuthStore((state) => state.isGuest);
|
|
63
|
+
const error = useAuthStore((state) => state.error);
|
|
64
|
+
const isAuthenticated = useAuthStore(selectIsAuthenticated);
|
|
65
|
+
|
|
66
|
+
// Actions from store
|
|
67
|
+
const setLoading = useAuthStore((state) => state.setLoading);
|
|
68
|
+
const setError = useAuthStore((state) => state.setError);
|
|
69
|
+
const setIsGuest = useAuthStore((state) => state.setIsGuest);
|
|
70
|
+
|
|
71
|
+
// Mutations
|
|
72
|
+
const signInMutation = useSignInMutation();
|
|
73
|
+
const signUpMutation = useSignUpMutation();
|
|
74
|
+
const signOutMutation = useSignOutMutation();
|
|
75
|
+
const guestModeMutation = useGuestModeMutation();
|
|
76
|
+
|
|
77
|
+
const signUp = useCallback(
|
|
78
|
+
async (email: string, password: string, displayName?: string) => {
|
|
79
|
+
try {
|
|
80
|
+
setLoading(true);
|
|
81
|
+
setError(null);
|
|
82
|
+
await signUpMutation.mutateAsync({ email, password, displayName });
|
|
83
|
+
if (isGuest) {
|
|
84
|
+
setIsGuest(false);
|
|
85
|
+
}
|
|
86
|
+
} catch (err: unknown) {
|
|
87
|
+
const errorMessage = err instanceof Error ? err.message : "Sign up failed";
|
|
88
|
+
setError(errorMessage);
|
|
89
|
+
throw err;
|
|
90
|
+
} finally {
|
|
91
|
+
setLoading(false);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
[isGuest, setIsGuest, setLoading, setError, signUpMutation]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const signIn = useCallback(
|
|
98
|
+
async (email: string, password: string) => {
|
|
99
|
+
try {
|
|
100
|
+
setLoading(true);
|
|
101
|
+
setError(null);
|
|
102
|
+
await signInMutation.mutateAsync({ email, password });
|
|
103
|
+
if (isGuest) {
|
|
104
|
+
setIsGuest(false);
|
|
105
|
+
}
|
|
106
|
+
} catch (err: unknown) {
|
|
107
|
+
const errorMessage = err instanceof Error ? err.message : "Sign in failed";
|
|
108
|
+
setError(errorMessage);
|
|
109
|
+
throw err;
|
|
110
|
+
} finally {
|
|
111
|
+
setLoading(false);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
[isGuest, setIsGuest, setLoading, setError, signInMutation]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const signOut = useCallback(async () => {
|
|
118
|
+
try {
|
|
119
|
+
setLoading(true);
|
|
120
|
+
await signOutMutation.mutateAsync();
|
|
121
|
+
} finally {
|
|
122
|
+
setLoading(false);
|
|
123
|
+
}
|
|
124
|
+
}, [setLoading, signOutMutation]);
|
|
125
|
+
|
|
126
|
+
const continueAsGuest = useCallback(async () => {
|
|
127
|
+
try {
|
|
128
|
+
setLoading(true);
|
|
129
|
+
await guestModeMutation.mutateAsync();
|
|
130
|
+
setIsGuest(true);
|
|
131
|
+
} catch {
|
|
132
|
+
setIsGuest(true);
|
|
133
|
+
} finally {
|
|
134
|
+
setLoading(false);
|
|
135
|
+
}
|
|
136
|
+
}, [setIsGuest, setLoading, guestModeMutation]);
|
|
52
137
|
|
|
53
138
|
return {
|
|
54
|
-
user
|
|
55
|
-
loading
|
|
56
|
-
isGuest
|
|
57
|
-
isAuthenticated
|
|
58
|
-
error
|
|
59
|
-
signUp
|
|
60
|
-
signIn
|
|
61
|
-
signOut
|
|
62
|
-
continueAsGuest
|
|
63
|
-
setError
|
|
139
|
+
user,
|
|
140
|
+
loading,
|
|
141
|
+
isGuest,
|
|
142
|
+
isAuthenticated,
|
|
143
|
+
error,
|
|
144
|
+
signUp,
|
|
145
|
+
signIn,
|
|
146
|
+
signOut,
|
|
147
|
+
continueAsGuest,
|
|
148
|
+
setError,
|
|
64
149
|
};
|
|
65
150
|
}
|
|
@@ -53,12 +53,11 @@ export const AccountScreen: React.FC<AccountScreenProps> = ({ config }) => {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
const styles = StyleSheet.create({
|
|
56
|
-
},
|
|
57
56
|
content: {
|
|
58
|
-
|
|
59
|
-
},
|
|
57
|
+
padding: 16,
|
|
58
|
+
},
|
|
60
59
|
divider: {
|
|
61
|
-
|
|
62
|
-
},
|
|
60
|
+
height: 24,
|
|
61
|
+
},
|
|
63
62
|
});
|
|
64
63
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Store
|
|
3
|
+
* Centralized auth state management using Zustand with AsyncStorage persistence
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for auth state across the app.
|
|
6
|
+
* Firebase auth changes are synced via initializeAuthListener().
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Initialize once in app root
|
|
11
|
+
* useEffect(() => {
|
|
12
|
+
* const unsubscribe = initializeAuthListener();
|
|
13
|
+
* return unsubscribe;
|
|
14
|
+
* }, []);
|
|
15
|
+
*
|
|
16
|
+
* // Use anywhere
|
|
17
|
+
* const { user, isAuthenticated, signIn, signOut } = useAuthStore();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createStore } from "@umituz/react-native-storage";
|
|
22
|
+
import { onAuthStateChanged, type User } from "firebase/auth";
|
|
23
|
+
import { getFirebaseAuth } from "@umituz/react-native-firebase";
|
|
24
|
+
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
25
|
+
import { mapToAuthUser } from "../../infrastructure/utils/UserMapper";
|
|
26
|
+
import { getAuthService } from "../../infrastructure/services/AuthService";
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// STATE TYPES
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
interface AuthState {
|
|
33
|
+
/** Mapped AuthUser (null if not authenticated) */
|
|
34
|
+
user: AuthUser | null;
|
|
35
|
+
/** Raw Firebase user reference */
|
|
36
|
+
firebaseUser: User | null;
|
|
37
|
+
/** Loading state during auth operations */
|
|
38
|
+
loading: boolean;
|
|
39
|
+
/** Guest mode (user skipped authentication) */
|
|
40
|
+
isGuest: boolean;
|
|
41
|
+
/** Error message from last auth operation */
|
|
42
|
+
error: string | null;
|
|
43
|
+
/** Whether auth listener has initialized */
|
|
44
|
+
initialized: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface AuthActions {
|
|
48
|
+
/** Update user from Firebase listener */
|
|
49
|
+
setFirebaseUser: (user: User | null) => void;
|
|
50
|
+
/** Set loading state */
|
|
51
|
+
setLoading: (loading: boolean) => void;
|
|
52
|
+
/** Set guest mode */
|
|
53
|
+
setIsGuest: (isGuest: boolean) => void;
|
|
54
|
+
/** Set error message */
|
|
55
|
+
setError: (error: string | null) => void;
|
|
56
|
+
/** Mark as initialized */
|
|
57
|
+
setInitialized: (initialized: boolean) => void;
|
|
58
|
+
/** Reset to initial state */
|
|
59
|
+
reset: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// INITIAL STATE
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
const initialState: AuthState = {
|
|
67
|
+
user: null,
|
|
68
|
+
firebaseUser: null,
|
|
69
|
+
loading: true,
|
|
70
|
+
isGuest: false,
|
|
71
|
+
error: null,
|
|
72
|
+
initialized: false,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// STORE
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
export const useAuthStore = createStore<AuthState, AuthActions>({
|
|
80
|
+
name: "auth-store",
|
|
81
|
+
initialState,
|
|
82
|
+
persist: true,
|
|
83
|
+
version: 1,
|
|
84
|
+
partialize: (state) => ({
|
|
85
|
+
// Only persist these fields (not functions, not firebaseUser)
|
|
86
|
+
isGuest: state.isGuest,
|
|
87
|
+
initialized: state.initialized,
|
|
88
|
+
}),
|
|
89
|
+
actions: (set, get) => ({
|
|
90
|
+
setFirebaseUser: (firebaseUser) => {
|
|
91
|
+
const { isGuest } = get();
|
|
92
|
+
|
|
93
|
+
let user: AuthUser | null = null;
|
|
94
|
+
|
|
95
|
+
if (firebaseUser) {
|
|
96
|
+
// Non-anonymous users always get mapped
|
|
97
|
+
if (!firebaseUser.isAnonymous) {
|
|
98
|
+
user = mapToAuthUser(firebaseUser);
|
|
99
|
+
}
|
|
100
|
+
// Anonymous users only if not in guest mode
|
|
101
|
+
else if (!isGuest) {
|
|
102
|
+
user = mapToAuthUser(firebaseUser);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
set({
|
|
107
|
+
firebaseUser,
|
|
108
|
+
user,
|
|
109
|
+
loading: false,
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
setLoading: (loading) => set({ loading }),
|
|
114
|
+
|
|
115
|
+
setIsGuest: (isGuest) => {
|
|
116
|
+
const { firebaseUser } = get();
|
|
117
|
+
|
|
118
|
+
// Recalculate user when guest mode changes
|
|
119
|
+
let user: AuthUser | null = null;
|
|
120
|
+
if (firebaseUser) {
|
|
121
|
+
if (!firebaseUser.isAnonymous) {
|
|
122
|
+
user = mapToAuthUser(firebaseUser);
|
|
123
|
+
} else if (!isGuest) {
|
|
124
|
+
user = mapToAuthUser(firebaseUser);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
set({ isGuest, user });
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
setError: (error) => set({ error }),
|
|
132
|
+
|
|
133
|
+
setInitialized: (initialized) => set({ initialized }),
|
|
134
|
+
|
|
135
|
+
reset: () => set(initialState),
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// SELECTORS
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if user is authenticated (not guest, not anonymous)
|
|
145
|
+
*/
|
|
146
|
+
export const selectIsAuthenticated = (state: AuthState): boolean => {
|
|
147
|
+
return !!state.user && !state.isGuest && !state.user.isAnonymous;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// LISTENER
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
let listenerInitialized = false;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Initialize Firebase auth listener
|
|
158
|
+
* Call once in app root, returns unsubscribe function
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```typescript
|
|
162
|
+
* useEffect(() => {
|
|
163
|
+
* const unsubscribe = initializeAuthListener();
|
|
164
|
+
* return unsubscribe;
|
|
165
|
+
* }, []);
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function initializeAuthListener(): () => void {
|
|
169
|
+
// Prevent multiple initializations
|
|
170
|
+
if (listenerInitialized) {
|
|
171
|
+
return () => {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const auth = getFirebaseAuth();
|
|
175
|
+
const store = useAuthStore.getState();
|
|
176
|
+
|
|
177
|
+
if (!auth) {
|
|
178
|
+
store.setLoading(false);
|
|
179
|
+
store.setInitialized(true);
|
|
180
|
+
return () => {};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Sync initial guest mode from service
|
|
184
|
+
const service = getAuthService();
|
|
185
|
+
if (service) {
|
|
186
|
+
const isGuest = service.getIsGuestMode();
|
|
187
|
+
if (isGuest) {
|
|
188
|
+
store.setIsGuest(true);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
listenerInitialized = true;
|
|
193
|
+
|
|
194
|
+
// Subscribe to auth state changes
|
|
195
|
+
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
|
196
|
+
if (__DEV__) {
|
|
197
|
+
console.log("[authStore] Auth state changed:", user?.uid ?? "null");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
store.setFirebaseUser(user);
|
|
201
|
+
store.setInitialized(true);
|
|
202
|
+
|
|
203
|
+
// Reset guest mode when real user signs in
|
|
204
|
+
if (user && !user.isAnonymous && store.isGuest) {
|
|
205
|
+
store.setIsGuest(false);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return () => {
|
|
210
|
+
unsubscribe();
|
|
211
|
+
listenerInitialized = false;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Reset listener state (for testing)
|
|
217
|
+
*/
|
|
218
|
+
export function resetAuthListener(): void {
|
|
219
|
+
listenerInitialized = false;
|
|
220
|
+
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import type { UseAuthStateResult } from "./useAuthState";
|
|
3
|
-
import {
|
|
4
|
-
useSignInMutation,
|
|
5
|
-
useSignUpMutation,
|
|
6
|
-
useSignOutMutation,
|
|
7
|
-
useGuestModeMutation,
|
|
8
|
-
} from "./mutations/useAuthMutations";
|
|
9
|
-
|
|
10
|
-
export interface UseAuthActionsResult {
|
|
11
|
-
signUp: (email: string, password: string, displayName?: string) => Promise<void>;
|
|
12
|
-
signIn: (email: string, password: string) => Promise<void>;
|
|
13
|
-
signOut: () => Promise<void>;
|
|
14
|
-
continueAsGuest: () => Promise<void>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function useAuthActions(state: UseAuthStateResult): UseAuthActionsResult {
|
|
18
|
-
const { isGuest, setIsGuest, setLoading, setError } = state;
|
|
19
|
-
|
|
20
|
-
const signInMutation = useSignInMutation();
|
|
21
|
-
const signUpMutation = useSignUpMutation();
|
|
22
|
-
const signOutMutation = useSignOutMutation();
|
|
23
|
-
const guestModeMutation = useGuestModeMutation();
|
|
24
|
-
|
|
25
|
-
const signUp = useCallback(
|
|
26
|
-
async (email: string, password: string, displayName?: string) => {
|
|
27
|
-
try {
|
|
28
|
-
setLoading(true);
|
|
29
|
-
setError(null);
|
|
30
|
-
await signUpMutation.mutateAsync({ email, password, displayName });
|
|
31
|
-
if (isGuest) {
|
|
32
|
-
setIsGuest(false);
|
|
33
|
-
}
|
|
34
|
-
} catch (err: unknown) {
|
|
35
|
-
const errorMessage = err instanceof Error ? err.message : "Sign up failed";
|
|
36
|
-
setError(errorMessage);
|
|
37
|
-
throw err;
|
|
38
|
-
} finally {
|
|
39
|
-
setLoading(false);
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
[isGuest, setIsGuest, setLoading, setError, signUpMutation],
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const signIn = useCallback(
|
|
46
|
-
async (email: string, password: string) => {
|
|
47
|
-
try {
|
|
48
|
-
setLoading(true);
|
|
49
|
-
setError(null);
|
|
50
|
-
await signInMutation.mutateAsync({ email, password });
|
|
51
|
-
if (isGuest) {
|
|
52
|
-
setIsGuest(false);
|
|
53
|
-
}
|
|
54
|
-
} catch (err: unknown) {
|
|
55
|
-
const errorMessage = err instanceof Error ? err.message : "Sign in failed";
|
|
56
|
-
setError(errorMessage);
|
|
57
|
-
throw err;
|
|
58
|
-
} finally {
|
|
59
|
-
setLoading(false);
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
[isGuest, setIsGuest, setLoading, setError, signInMutation],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const signOut = useCallback(async () => {
|
|
66
|
-
try {
|
|
67
|
-
setLoading(true);
|
|
68
|
-
await signOutMutation.mutateAsync();
|
|
69
|
-
} finally {
|
|
70
|
-
setLoading(false);
|
|
71
|
-
}
|
|
72
|
-
}, [setLoading, signOutMutation]);
|
|
73
|
-
|
|
74
|
-
const continueAsGuest = useCallback(async () => {
|
|
75
|
-
try {
|
|
76
|
-
setLoading(true);
|
|
77
|
-
await guestModeMutation.mutateAsync();
|
|
78
|
-
setIsGuest(true);
|
|
79
|
-
} catch {
|
|
80
|
-
setIsGuest(true);
|
|
81
|
-
} finally {
|
|
82
|
-
setLoading(false);
|
|
83
|
-
}
|
|
84
|
-
}, [setIsGuest, setLoading, guestModeMutation]);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
signUp,
|
|
88
|
-
signIn,
|
|
89
|
-
signOut,
|
|
90
|
-
continueAsGuest,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useAuthState Hook
|
|
3
|
-
* Single Responsibility: Manage authentication state
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument */
|
|
7
|
-
import { useState, useEffect, useRef, useMemo } from "react";
|
|
8
|
-
import { DeviceEventEmitter } from "react-native";
|
|
9
|
-
import { getAuthService } from "../../infrastructure/services/AuthService";
|
|
10
|
-
import { useFirebaseAuth } from "@umituz/react-native-firebase";
|
|
11
|
-
import { mapToAuthUser } from "../../infrastructure/utils/UserMapper";
|
|
12
|
-
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
13
|
-
|
|
14
|
-
export interface UseAuthStateResult {
|
|
15
|
-
user: AuthUser | null;
|
|
16
|
-
isAuthenticated: boolean;
|
|
17
|
-
isGuest: boolean;
|
|
18
|
-
loading: boolean;
|
|
19
|
-
error: string | null;
|
|
20
|
-
setError: (error: string | null) => void;
|
|
21
|
-
setIsGuest: (isGuest: boolean) => void;
|
|
22
|
-
setLoading: (loading: boolean) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Hook for managing authentication state
|
|
27
|
-
*/
|
|
28
|
-
export function useAuthState(): UseAuthStateResult {
|
|
29
|
-
const { user: firebaseUser, loading: firebaseLoading } = useFirebaseAuth();
|
|
30
|
-
const [isGuest, setIsGuest] = useState(() => {
|
|
31
|
-
const service = getAuthService();
|
|
32
|
-
return service ? service.getIsGuestMode() : false;
|
|
33
|
-
});
|
|
34
|
-
const [error, setError] = useState<string | null>(null);
|
|
35
|
-
const [loading, setLoading] = useState(false);
|
|
36
|
-
|
|
37
|
-
// Ref to track latest isGuest value for event handlers
|
|
38
|
-
const isGuestRef = useRef(isGuest);
|
|
39
|
-
|
|
40
|
-
// Memoize user to prevent new object reference on every render
|
|
41
|
-
const user = useMemo(() => {
|
|
42
|
-
// If no Firebase user, return null
|
|
43
|
-
if (!firebaseUser) return null;
|
|
44
|
-
|
|
45
|
-
// If Firebase user exists and is NOT anonymous, always return the user
|
|
46
|
-
// This ensures real authenticated users (email, Google, Apple) always have user object
|
|
47
|
-
// even if isGuest was previously true (which gets reset by useEffect below)
|
|
48
|
-
if (!firebaseUser.isAnonymous) {
|
|
49
|
-
return mapToAuthUser(firebaseUser);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// If Firebase user is anonymous AND we're in guest mode, return null
|
|
53
|
-
// Guest mode = user clicked "Continue as Guest" with no account
|
|
54
|
-
if (isGuest) return null;
|
|
55
|
-
|
|
56
|
-
// If Firebase user is anonymous but NOT in guest mode, return the user
|
|
57
|
-
// This handles anonymous auth accounts that can be upgraded later
|
|
58
|
-
return mapToAuthUser(firebaseUser);
|
|
59
|
-
}, [isGuest, firebaseUser?.uid, firebaseUser?.isAnonymous]);
|
|
60
|
-
|
|
61
|
-
// Anonymous users are NOT authenticated - they need to register/login
|
|
62
|
-
const isAuthenticated = !!user && !isGuest && !user.isAnonymous;
|
|
63
|
-
|
|
64
|
-
// Keep ref in sync with state
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
isGuestRef.current = isGuest;
|
|
67
|
-
}, [isGuest]);
|
|
68
|
-
|
|
69
|
-
// Reset guest mode when user signs in
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
if (firebaseUser && isGuest) {
|
|
72
|
-
setIsGuest(false);
|
|
73
|
-
}
|
|
74
|
-
}, [firebaseUser, isGuest]);
|
|
75
|
-
|
|
76
|
-
// Sync isGuest state with service on mount only
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
const service = getAuthService();
|
|
79
|
-
if (service) {
|
|
80
|
-
const serviceIsGuest = service.getIsGuestMode();
|
|
81
|
-
if (serviceIsGuest !== isGuestRef.current) {
|
|
82
|
-
setIsGuest(serviceIsGuest);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}, []);
|
|
86
|
-
|
|
87
|
-
// Listen for auth events - subscribe once on mount
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
const guestSubscription = DeviceEventEmitter.addListener(
|
|
90
|
-
"guest-mode-enabled",
|
|
91
|
-
() => setIsGuest(true)
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const authSubscription = DeviceEventEmitter.addListener(
|
|
95
|
-
"user-authenticated",
|
|
96
|
-
() => {
|
|
97
|
-
if (isGuestRef.current) {
|
|
98
|
-
setIsGuest(false);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const errorSubscription = DeviceEventEmitter.addListener(
|
|
104
|
-
"auth-error",
|
|
105
|
-
(payload: { error?: string }) => {
|
|
106
|
-
if (payload?.error) {
|
|
107
|
-
setError(payload.error);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return () => {
|
|
114
|
-
guestSubscription.remove();
|
|
115
|
-
authSubscription.remove();
|
|
116
|
-
errorSubscription.remove();
|
|
117
|
-
};
|
|
118
|
-
}, []);
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
user,
|
|
122
|
-
isAuthenticated,
|
|
123
|
-
isGuest,
|
|
124
|
-
loading: loading || firebaseLoading,
|
|
125
|
-
error,
|
|
126
|
-
setError,
|
|
127
|
-
setIsGuest,
|
|
128
|
-
setLoading,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|