@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-auth",
3
- "version": "2.7.7",
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 provider-agnostic AuthUser type.
6
- * Adds app-specific state (guest mode, error handling) on top of provider auth.
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 { useAuthState } from "./useAuthState";
10
- import { useAuthActions } from "./useAuthActions";
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 Firebase Auth's built-in state management and adds app-specific features:
40
- * - Guest mode support
41
- * - Error handling
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
- const state = useAuthState();
51
- const actions = useAuthActions(state);
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: state.user,
55
- loading: state.loading,
56
- isGuest: state.isGuest,
57
- isAuthenticated: state.isAuthenticated,
58
- error: state.error,
59
- signUp: actions.signUp,
60
- signIn: actions.signIn,
61
- signOut: actions.signOut,
62
- continueAsGuest: actions.continueAsGuest,
63
- setError: state.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
- padding: 16,
59
- },
57
+ padding: 16,
58
+ },
60
59
  divider: {
61
- height: 24,
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
-