@umituz/react-native-auth 3.2.0 → 3.2.2
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 +6 -2
- package/src/index.ts +17 -1
- package/src/infrastructure/services/AuthService.ts +0 -1
- package/src/presentation/components/AuthBottomSheetWrapper.tsx +115 -0
- package/src/presentation/hooks/mutations/useAuthMutations.ts +3 -4
- package/src/presentation/hooks/useAppleAuth.ts +61 -0
- package/src/presentation/hooks/useGoogleAuth.ts +184 -0
- package/src/presentation/hooks/useSocialLogin.ts +115 -0
- package/src/presentation/stores/auth.selectors.ts +48 -0
- package/src/presentation/stores/authStore.ts +30 -166
- package/src/presentation/stores/initializeAuthListener.ts +113 -0
- package/src/types/auth-store.types.ts +70 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.2",
|
|
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",
|
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
"@umituz/react-native-storage": "*",
|
|
44
44
|
"@umituz/react-native-tanstack": "*",
|
|
45
45
|
"@umituz/react-native-validation": "*",
|
|
46
|
+
"expo-auth-session": ">=5.0.0",
|
|
46
47
|
"expo-linear-gradient": ">=13.0.0",
|
|
48
|
+
"expo-web-browser": ">=12.0.0",
|
|
47
49
|
"firebase": ">=11.0.0",
|
|
48
50
|
"react": ">=18.2.0",
|
|
49
51
|
"react-native": ">=0.74.0",
|
|
@@ -68,7 +70,7 @@
|
|
|
68
70
|
"@types/react": "~19.1.0",
|
|
69
71
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
70
72
|
"@typescript-eslint/parser": "^7.0.0",
|
|
71
|
-
"@umituz/react-native-design-system": "
|
|
73
|
+
"@umituz/react-native-design-system": "latest",
|
|
72
74
|
"@umituz/react-native-design-system-theme": "*",
|
|
73
75
|
"@umituz/react-native-firebase": "^1.13.27",
|
|
74
76
|
"@umituz/react-native-haptics": "*",
|
|
@@ -86,8 +88,10 @@
|
|
|
86
88
|
"expo-device": "^8.0.10",
|
|
87
89
|
"expo-file-system": "^19.0.21",
|
|
88
90
|
"expo-haptics": "^15.0.8",
|
|
91
|
+
"expo-auth-session": "^5.0.0",
|
|
89
92
|
"expo-linear-gradient": "^13.0.0",
|
|
90
93
|
"expo-sharing": "^14.0.8",
|
|
94
|
+
"expo-web-browser": "^12.0.0",
|
|
91
95
|
"firebase": "^11.0.0",
|
|
92
96
|
"react": "~19.1.0",
|
|
93
97
|
"react-native": "~0.81.5",
|
package/src/index.ts
CHANGED
|
@@ -135,6 +135,15 @@ export type { UseProfileUpdateReturn } from './presentation/hooks/useProfileUpda
|
|
|
135
135
|
export { useProfileEdit } from './presentation/hooks/useProfileEdit';
|
|
136
136
|
export type { UseProfileEditReturn, ProfileEditFormState } from './presentation/hooks/useProfileEdit';
|
|
137
137
|
|
|
138
|
+
export { useSocialLogin } from './presentation/hooks/useSocialLogin';
|
|
139
|
+
export type { UseSocialLoginConfig, UseSocialLoginResult } from './presentation/hooks/useSocialLogin';
|
|
140
|
+
|
|
141
|
+
export { useGoogleAuth } from './presentation/hooks/useGoogleAuth';
|
|
142
|
+
export type { UseGoogleAuthResult, GoogleAuthConfig as GoogleAuthHookConfig } from './presentation/hooks/useGoogleAuth';
|
|
143
|
+
|
|
144
|
+
export { useAppleAuth } from './presentation/hooks/useAppleAuth';
|
|
145
|
+
export type { UseAppleAuthResult } from './presentation/hooks/useAppleAuth';
|
|
146
|
+
|
|
138
147
|
export type { UserProfile, UpdateProfileParams } from './domain/entities/UserProfile';
|
|
139
148
|
|
|
140
149
|
// Domain Utils - Guest Names
|
|
@@ -175,6 +184,11 @@ export { PasswordMatchIndicator } from './presentation/components/PasswordMatchI
|
|
|
175
184
|
export type { PasswordMatchIndicatorProps } from './presentation/components/PasswordMatchIndicator';
|
|
176
185
|
export { AuthBottomSheet } from './presentation/components/AuthBottomSheet';
|
|
177
186
|
export type { AuthBottomSheetProps } from './presentation/components/AuthBottomSheet';
|
|
187
|
+
export { AuthBottomSheetWrapper } from './presentation/components/AuthBottomSheetWrapper';
|
|
188
|
+
export type {
|
|
189
|
+
AuthBottomSheetWrapperProps,
|
|
190
|
+
SocialAuthConfiguration,
|
|
191
|
+
} from './presentation/components/AuthBottomSheetWrapper';
|
|
178
192
|
export { SocialLoginButtons } from './presentation/components/SocialLoginButtons';
|
|
179
193
|
export type { SocialLoginButtonsProps } from './presentation/components/SocialLoginButtons';
|
|
180
194
|
export { ProfileSection } from './presentation/components/ProfileSection';
|
|
@@ -193,6 +207,7 @@ export {
|
|
|
193
207
|
useAuthStore,
|
|
194
208
|
initializeAuthListener,
|
|
195
209
|
resetAuthListener,
|
|
210
|
+
isAuthListenerInitialized,
|
|
196
211
|
selectIsAuthenticated,
|
|
197
212
|
selectUserId,
|
|
198
213
|
selectIsAnonymous,
|
|
@@ -205,7 +220,8 @@ export {
|
|
|
205
220
|
getIsAnonymous,
|
|
206
221
|
} from './presentation/stores/authStore';
|
|
207
222
|
|
|
208
|
-
export type { UserType } from './presentation/stores/authStore';
|
|
223
|
+
export type { UserType, AuthState, AuthActions } from './presentation/stores/authStore';
|
|
224
|
+
export type { AuthListenerOptions } from './types/auth-store.types';
|
|
209
225
|
|
|
210
226
|
// =============================================================================
|
|
211
227
|
// PRESENTATION LAYER - Utilities
|
|
@@ -146,7 +146,6 @@ export class AuthService implements IAuthService {
|
|
|
146
146
|
|
|
147
147
|
getConfig(): AuthConfig { return this.config; }
|
|
148
148
|
getGuestModeService(): GuestModeService { return this.guestModeService; }
|
|
149
|
-
getRepository(): AuthRepository { return this.repositoryInstance; }
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
let authServiceInstance: AuthService | null = null;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthBottomSheetWrapper Component
|
|
3
|
+
* Self-contained auth modal with integrated social login
|
|
4
|
+
*
|
|
5
|
+
* This wrapper component combines AuthBottomSheet with social auth hooks,
|
|
6
|
+
* providing a complete authentication solution for apps.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <AuthBottomSheetWrapper
|
|
11
|
+
* termsUrl="https://myapp.com/terms"
|
|
12
|
+
* privacyUrl="https://myapp.com/privacy"
|
|
13
|
+
* socialConfig={{
|
|
14
|
+
* google: { webClientId: '...', iosClientId: '...' },
|
|
15
|
+
* apple: { enabled: true }
|
|
16
|
+
* }}
|
|
17
|
+
* />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import React, { useCallback, useMemo } from "react";
|
|
22
|
+
import { Platform } from "react-native";
|
|
23
|
+
import { AuthBottomSheet } from "./AuthBottomSheet";
|
|
24
|
+
import { useGoogleAuth, type GoogleAuthConfig } from "../hooks/useGoogleAuth";
|
|
25
|
+
import { useAppleAuth } from "../hooks/useAppleAuth";
|
|
26
|
+
import type { SocialAuthProvider } from "../../domain/value-objects/AuthConfig";
|
|
27
|
+
|
|
28
|
+
declare const __DEV__: boolean;
|
|
29
|
+
|
|
30
|
+
export interface SocialAuthConfiguration {
|
|
31
|
+
google?: GoogleAuthConfig;
|
|
32
|
+
apple?: { enabled: boolean };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AuthBottomSheetWrapperProps {
|
|
36
|
+
/** Terms of Service URL */
|
|
37
|
+
termsUrl?: string;
|
|
38
|
+
/** Privacy Policy URL */
|
|
39
|
+
privacyUrl?: string;
|
|
40
|
+
/** Called when Terms link is pressed (overrides default behavior) */
|
|
41
|
+
onTermsPress?: () => void;
|
|
42
|
+
/** Called when Privacy link is pressed (overrides default behavior) */
|
|
43
|
+
onPrivacyPress?: () => void;
|
|
44
|
+
/** Social auth configuration */
|
|
45
|
+
socialConfig?: SocialAuthConfiguration;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Self-contained auth bottom sheet with integrated social login
|
|
50
|
+
*/
|
|
51
|
+
export const AuthBottomSheetWrapper: React.FC<AuthBottomSheetWrapperProps> = ({
|
|
52
|
+
termsUrl,
|
|
53
|
+
privacyUrl,
|
|
54
|
+
onTermsPress,
|
|
55
|
+
onPrivacyPress,
|
|
56
|
+
socialConfig,
|
|
57
|
+
}) => {
|
|
58
|
+
const { signInWithGoogle, googleConfigured } = useGoogleAuth(socialConfig?.google);
|
|
59
|
+
const { signInWithApple, appleAvailable } = useAppleAuth();
|
|
60
|
+
|
|
61
|
+
const providers = useMemo<SocialAuthProvider[]>(() => {
|
|
62
|
+
const result: SocialAuthProvider[] = [];
|
|
63
|
+
|
|
64
|
+
if (Platform.OS === "ios" && socialConfig?.apple?.enabled && appleAvailable) {
|
|
65
|
+
result.push("apple");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (googleConfigured) {
|
|
69
|
+
result.push("google");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (__DEV__) {
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.log("[AuthBottomSheetWrapper] Enabled providers:", result);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}, [socialConfig?.apple?.enabled, appleAvailable, googleConfigured]);
|
|
79
|
+
|
|
80
|
+
const handleGoogleSignIn = useCallback(async () => {
|
|
81
|
+
if (__DEV__) {
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.log("[AuthBottomSheetWrapper] Google sign-in requested");
|
|
84
|
+
}
|
|
85
|
+
const result = await signInWithGoogle();
|
|
86
|
+
if (__DEV__) {
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.log("[AuthBottomSheetWrapper] Google result:", result);
|
|
89
|
+
}
|
|
90
|
+
}, [signInWithGoogle]);
|
|
91
|
+
|
|
92
|
+
const handleAppleSignIn = useCallback(async () => {
|
|
93
|
+
if (__DEV__) {
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.log("[AuthBottomSheetWrapper] Apple sign-in requested");
|
|
96
|
+
}
|
|
97
|
+
const result = await signInWithApple();
|
|
98
|
+
if (__DEV__) {
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.log("[AuthBottomSheetWrapper] Apple result:", result);
|
|
101
|
+
}
|
|
102
|
+
}, [signInWithApple]);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<AuthBottomSheet
|
|
106
|
+
termsUrl={termsUrl}
|
|
107
|
+
privacyUrl={privacyUrl}
|
|
108
|
+
onTermsPress={onTermsPress}
|
|
109
|
+
onPrivacyPress={onPrivacyPress}
|
|
110
|
+
socialProviders={providers}
|
|
111
|
+
onGoogleSignIn={handleGoogleSignIn}
|
|
112
|
+
onAppleSignIn={handleAppleSignIn}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -13,8 +13,7 @@ export const useSignUpMutation = () => {
|
|
|
13
13
|
mutationFn: async (params: SignUpParams): Promise<AuthUser> => {
|
|
14
14
|
const service = getAuthService();
|
|
15
15
|
if (!service) throw new Error("Auth Service not initialized");
|
|
16
|
-
|
|
17
|
-
return service.getRepository().signUp(params);
|
|
16
|
+
return service.signUp(params);
|
|
18
17
|
},
|
|
19
18
|
});
|
|
20
19
|
};
|
|
@@ -24,7 +23,7 @@ export const useSignInMutation = () => {
|
|
|
24
23
|
mutationFn: async (params: SignInParams): Promise<AuthUser> => {
|
|
25
24
|
const service = getAuthService();
|
|
26
25
|
if (!service) throw new Error("Auth Service not initialized");
|
|
27
|
-
return service.
|
|
26
|
+
return service.signIn(params);
|
|
28
27
|
},
|
|
29
28
|
});
|
|
30
29
|
};
|
|
@@ -34,7 +33,7 @@ export const useSignOutMutation = () => {
|
|
|
34
33
|
mutationFn: async (): Promise<void> => {
|
|
35
34
|
const service = getAuthService();
|
|
36
35
|
if (!service) throw new Error("Auth Service not initialized");
|
|
37
|
-
return service.
|
|
36
|
+
return service.signOut();
|
|
38
37
|
},
|
|
39
38
|
});
|
|
40
39
|
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAppleAuth Hook
|
|
3
|
+
* Handles Apple Sign-In using Firebase auth
|
|
4
|
+
*
|
|
5
|
+
* This is a convenience wrapper around useSocialAuth from Firebase package
|
|
6
|
+
* specifically for Apple authentication.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback } from "react";
|
|
10
|
+
import { Platform } from "react-native";
|
|
11
|
+
import {
|
|
12
|
+
useSocialAuth,
|
|
13
|
+
type SocialAuthResult,
|
|
14
|
+
} from "@umituz/react-native-firebase";
|
|
15
|
+
|
|
16
|
+
declare const __DEV__: boolean;
|
|
17
|
+
|
|
18
|
+
export interface UseAppleAuthResult {
|
|
19
|
+
signInWithApple: () => Promise<SocialAuthResult>;
|
|
20
|
+
appleLoading: boolean;
|
|
21
|
+
appleAvailable: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook for Apple authentication
|
|
26
|
+
*/
|
|
27
|
+
export function useAppleAuth(): UseAppleAuthResult {
|
|
28
|
+
const { signInWithApple, appleLoading, appleAvailable } = useSocialAuth({
|
|
29
|
+
apple: { enabled: Platform.OS === "ios" },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const handleSignInWithApple = useCallback(async (): Promise<SocialAuthResult> => {
|
|
33
|
+
if (Platform.OS !== "ios") {
|
|
34
|
+
return { success: false, error: "Apple Sign-In is only available on iOS" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!appleAvailable) {
|
|
38
|
+
return { success: false, error: "Apple Sign-In is not available" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (__DEV__) {
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.log("[useAppleAuth] Apple sign-in requested");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await signInWithApple();
|
|
47
|
+
|
|
48
|
+
if (__DEV__) {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.log("[useAppleAuth] Apple sign-in result:", result);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}, [appleAvailable, signInWithApple]);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
signInWithApple: handleSignInWithApple,
|
|
58
|
+
appleLoading,
|
|
59
|
+
appleAvailable,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGoogleAuth Hook
|
|
3
|
+
* Handles Google OAuth flow using expo-auth-session and Firebase auth
|
|
4
|
+
*
|
|
5
|
+
* This hook provides complete Google sign-in flow:
|
|
6
|
+
* 1. OAuth flow via expo-auth-session
|
|
7
|
+
* 2. Firebase authentication with the obtained token
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useCallback, useEffect } from "react";
|
|
11
|
+
import {
|
|
12
|
+
useSocialAuth,
|
|
13
|
+
type SocialAuthConfig,
|
|
14
|
+
type SocialAuthResult,
|
|
15
|
+
} from "@umituz/react-native-firebase";
|
|
16
|
+
|
|
17
|
+
declare const __DEV__: boolean;
|
|
18
|
+
|
|
19
|
+
// Type declarations for expo-auth-session
|
|
20
|
+
interface GoogleAuthRequestConfig {
|
|
21
|
+
iosClientId: string;
|
|
22
|
+
webClientId: string;
|
|
23
|
+
androidClientId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface AuthSessionAuthentication {
|
|
27
|
+
idToken?: string;
|
|
28
|
+
accessToken?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AuthSessionResult {
|
|
32
|
+
type: "success" | "cancel" | "dismiss" | "error" | "locked";
|
|
33
|
+
authentication?: AuthSessionAuthentication;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface AuthRequest {
|
|
37
|
+
promptAsync: () => Promise<AuthSessionResult>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type UseAuthRequestReturn = [
|
|
41
|
+
AuthRequest | null,
|
|
42
|
+
AuthSessionResult | null,
|
|
43
|
+
() => Promise<AuthSessionResult>,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Dynamic imports to handle optional dependencies
|
|
47
|
+
type GoogleModule = { useAuthRequest: (config: GoogleAuthRequestConfig) => UseAuthRequestReturn };
|
|
48
|
+
type WebBrowserModule = { maybeCompleteAuthSession: () => void };
|
|
49
|
+
|
|
50
|
+
let Google: GoogleModule | null = null;
|
|
51
|
+
let WebBrowser: WebBrowserModule | null = null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
55
|
+
Google = require("expo-auth-session/providers/google") as GoogleModule;
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
57
|
+
WebBrowser = require("expo-web-browser") as WebBrowserModule;
|
|
58
|
+
if (WebBrowser) {
|
|
59
|
+
WebBrowser.maybeCompleteAuthSession();
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// expo-auth-session not available
|
|
63
|
+
if (__DEV__) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log("[useGoogleAuth] expo-auth-session not available");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface GoogleAuthConfig {
|
|
70
|
+
iosClientId?: string;
|
|
71
|
+
webClientId?: string;
|
|
72
|
+
androidClientId?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface UseGoogleAuthResult {
|
|
76
|
+
signInWithGoogle: () => Promise<SocialAuthResult>;
|
|
77
|
+
googleLoading: boolean;
|
|
78
|
+
googleConfigured: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const PLACEHOLDER_CLIENT_ID = "000000000000-placeholder.apps.googleusercontent.com";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Hook for Google authentication with expo-auth-session
|
|
85
|
+
*/
|
|
86
|
+
export function useGoogleAuth(config?: GoogleAuthConfig): UseGoogleAuthResult {
|
|
87
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
88
|
+
|
|
89
|
+
const googleConfigured = !!(
|
|
90
|
+
config?.iosClientId ||
|
|
91
|
+
config?.webClientId ||
|
|
92
|
+
config?.androidClientId
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const socialAuthConfig: SocialAuthConfig = {
|
|
96
|
+
google: config,
|
|
97
|
+
apple: { enabled: false },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const { signInWithGoogleToken, googleLoading: firebaseLoading } =
|
|
101
|
+
useSocialAuth(socialAuthConfig);
|
|
102
|
+
|
|
103
|
+
// Use Google auth request if available
|
|
104
|
+
const authRequest = Google?.useAuthRequest({
|
|
105
|
+
iosClientId: config?.iosClientId || PLACEHOLDER_CLIENT_ID,
|
|
106
|
+
webClientId: config?.webClientId || PLACEHOLDER_CLIENT_ID,
|
|
107
|
+
androidClientId: config?.androidClientId || PLACEHOLDER_CLIENT_ID,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const request = authRequest?.[0] ?? null;
|
|
111
|
+
const googleResponse = authRequest?.[1] ?? null;
|
|
112
|
+
const promptGoogleAsync = authRequest?.[2];
|
|
113
|
+
|
|
114
|
+
// Handle Google OAuth response
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (googleResponse?.type === "success") {
|
|
117
|
+
const idToken = googleResponse.authentication?.idToken;
|
|
118
|
+
if (idToken) {
|
|
119
|
+
setIsLoading(true);
|
|
120
|
+
signInWithGoogleToken(idToken)
|
|
121
|
+
.catch((error: unknown) => {
|
|
122
|
+
if (__DEV__) {
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.error("[useGoogleAuth] Firebase sign-in error:", error);
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
.finally(() => {
|
|
128
|
+
setIsLoading(false);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}, [googleResponse, signInWithGoogleToken]);
|
|
133
|
+
|
|
134
|
+
const signInWithGoogle = useCallback(async (): Promise<SocialAuthResult> => {
|
|
135
|
+
if (!Google || !promptGoogleAsync) {
|
|
136
|
+
return { success: false, error: "expo-auth-session is not available" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!googleConfigured) {
|
|
140
|
+
return { success: false, error: "Google Sign-In is not configured" };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!request) {
|
|
144
|
+
return { success: false, error: "Google Sign-In not ready" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
setIsLoading(true);
|
|
148
|
+
try {
|
|
149
|
+
const result = await promptGoogleAsync();
|
|
150
|
+
|
|
151
|
+
if (result.type === "success" && result.authentication?.idToken) {
|
|
152
|
+
const firebaseResult = await signInWithGoogleToken(
|
|
153
|
+
result.authentication.idToken,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (__DEV__) {
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.log("[useGoogleAuth] Sign-in successful:", firebaseResult);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return firebaseResult;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (result.type === "cancel") {
|
|
165
|
+
return { success: false, error: "Google Sign-In was cancelled" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { success: false, error: "Google Sign-In failed" };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
error: error instanceof Error ? error.message : "Google sign-in failed",
|
|
173
|
+
};
|
|
174
|
+
} finally {
|
|
175
|
+
setIsLoading(false);
|
|
176
|
+
}
|
|
177
|
+
}, [googleConfigured, request, promptGoogleAsync, signInWithGoogleToken]);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
signInWithGoogle,
|
|
181
|
+
googleLoading: isLoading || firebaseLoading,
|
|
182
|
+
googleConfigured,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSocialLogin Hook
|
|
3
|
+
* Provides unified social login functionality using Firebase package
|
|
4
|
+
*
|
|
5
|
+
* This hook wraps @umituz/react-native-firebase's useSocialAuth
|
|
6
|
+
* and provides a simple interface for social authentication.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const { signInWithGoogle, signInWithApple, googleLoading, appleLoading } = useSocialLogin({
|
|
11
|
+
* google: { webClientId: '...', iosClientId: '...' },
|
|
12
|
+
* apple: { enabled: true }
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useCallback } from "react";
|
|
18
|
+
import { Platform } from "react-native";
|
|
19
|
+
import {
|
|
20
|
+
useSocialAuth,
|
|
21
|
+
type SocialAuthConfig,
|
|
22
|
+
type SocialAuthResult,
|
|
23
|
+
} from "@umituz/react-native-firebase";
|
|
24
|
+
|
|
25
|
+
declare const __DEV__: boolean;
|
|
26
|
+
|
|
27
|
+
export interface UseSocialLoginConfig extends SocialAuthConfig {}
|
|
28
|
+
|
|
29
|
+
export interface UseSocialLoginResult {
|
|
30
|
+
/** Sign in with Google (handles OAuth flow internally if configured) */
|
|
31
|
+
signInWithGoogle: () => Promise<SocialAuthResult>;
|
|
32
|
+
/** Sign in with Apple */
|
|
33
|
+
signInWithApple: () => Promise<SocialAuthResult>;
|
|
34
|
+
/** Whether Google sign-in is in progress */
|
|
35
|
+
googleLoading: boolean;
|
|
36
|
+
/** Whether Apple sign-in is in progress */
|
|
37
|
+
appleLoading: boolean;
|
|
38
|
+
/** Whether Google is configured */
|
|
39
|
+
googleConfigured: boolean;
|
|
40
|
+
/** Whether Apple is available */
|
|
41
|
+
appleAvailable: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hook for social authentication
|
|
46
|
+
* Integrates with @umituz/react-native-firebase for Firebase auth
|
|
47
|
+
*/
|
|
48
|
+
export function useSocialLogin(config?: UseSocialLoginConfig): UseSocialLoginResult {
|
|
49
|
+
const {
|
|
50
|
+
signInWithApple: firebaseSignInWithApple,
|
|
51
|
+
googleLoading,
|
|
52
|
+
appleLoading,
|
|
53
|
+
googleConfigured,
|
|
54
|
+
appleAvailable,
|
|
55
|
+
} = useSocialAuth(config);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sign in with Google
|
|
59
|
+
* Note: For full OAuth flow, use useGoogleAuth hook which handles
|
|
60
|
+
* expo-auth-session OAuth flow and Firebase authentication
|
|
61
|
+
*/
|
|
62
|
+
const signInWithGoogle = useCallback((): Promise<SocialAuthResult> => {
|
|
63
|
+
if (!googleConfigured) {
|
|
64
|
+
return Promise.resolve({ success: false, error: "Google Sign-In is not configured" });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (__DEV__) {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.log("[useSocialLogin] Use useGoogleAuth hook for Google OAuth flow");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Promise.resolve({
|
|
73
|
+
success: false,
|
|
74
|
+
error: "Use useGoogleAuth hook for Google OAuth flow",
|
|
75
|
+
});
|
|
76
|
+
}, [googleConfigured]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sign in with Apple (full flow handled by Firebase package)
|
|
80
|
+
*/
|
|
81
|
+
const signInWithApple = useCallback(async (): Promise<SocialAuthResult> => {
|
|
82
|
+
if (Platform.OS !== "ios") {
|
|
83
|
+
return { success: false, error: "Apple Sign-In is only available on iOS" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!appleAvailable) {
|
|
87
|
+
return { success: false, error: "Apple Sign-In is not available" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (__DEV__) {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.log("[useSocialLogin] Apple sign-in requested");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await firebaseSignInWithApple();
|
|
96
|
+
|
|
97
|
+
if (__DEV__) {
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.log("[useSocialLogin] Apple sign-in result:", result);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}, [appleAvailable, firebaseSignInWithApple]);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
signInWithGoogle,
|
|
107
|
+
signInWithApple,
|
|
108
|
+
googleLoading,
|
|
109
|
+
appleLoading,
|
|
110
|
+
googleConfigured,
|
|
111
|
+
appleAvailable,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { type SocialAuthConfig, type SocialAuthResult };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Store Selectors
|
|
3
|
+
* Pure functions for deriving state from auth store
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AuthState, UserType } from "../../types/auth-store.types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get current user ID
|
|
10
|
+
*/
|
|
11
|
+
export const selectUserId = (state: AuthState): string | null => {
|
|
12
|
+
return state.firebaseUser?.uid ?? state.user?.uid ?? null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if user is authenticated (not guest, not anonymous)
|
|
17
|
+
*/
|
|
18
|
+
export const selectIsAuthenticated = (state: AuthState): boolean => {
|
|
19
|
+
return !!state.user && !state.isGuest && !state.user.isAnonymous;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if user is anonymous
|
|
24
|
+
*/
|
|
25
|
+
export const selectIsAnonymous = (state: AuthState): boolean => {
|
|
26
|
+
return state.firebaseUser?.isAnonymous ?? state.user?.isAnonymous ?? false;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get current user type
|
|
31
|
+
*/
|
|
32
|
+
export const selectUserType = (state: AuthState): UserType => {
|
|
33
|
+
if (!state.firebaseUser && !state.user) {
|
|
34
|
+
return "none";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isAnonymous =
|
|
38
|
+
state.firebaseUser?.isAnonymous ?? state.user?.isAnonymous ?? false;
|
|
39
|
+
|
|
40
|
+
return isAnonymous ? "anonymous" : "authenticated";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if auth is ready (initialized and not loading)
|
|
45
|
+
*/
|
|
46
|
+
export const selectIsAuthReady = (state: AuthState): boolean => {
|
|
47
|
+
return state.initialized && !state.loading;
|
|
48
|
+
};
|
|
@@ -1,79 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth Store
|
|
3
|
-
* Centralized auth state management using Zustand with
|
|
3
|
+
* Centralized auth state management using Zustand with persistence
|
|
4
4
|
*
|
|
5
5
|
* Single source of truth for auth state across the app.
|
|
6
6
|
* Firebase auth changes are synced via initializeAuthListener().
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createStore } from "@umituz/react-native-storage";
|
|
10
|
-
import { onAuthStateChanged, type User } from "firebase/auth";
|
|
11
|
-
import { getFirebaseAuth } from "@umituz/react-native-firebase";
|
|
12
10
|
import type { AuthUser } from "../../domain/entities/AuthUser";
|
|
13
11
|
import { mapToAuthUser } from "../../infrastructure/utils/UserMapper";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/** Guest mode (user skipped authentication) */
|
|
35
|
-
isGuest: boolean;
|
|
36
|
-
/** Error message from last auth operation */
|
|
37
|
-
error: string | null;
|
|
38
|
-
/** Whether auth listener has initialized */
|
|
39
|
-
initialized: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
interface AuthActions {
|
|
43
|
-
/** Update user from Firebase listener */
|
|
44
|
-
setFirebaseUser: (user: User | null) => void;
|
|
45
|
-
/** Set loading state */
|
|
46
|
-
setLoading: (loading: boolean) => void;
|
|
47
|
-
/** Set guest mode */
|
|
48
|
-
setIsGuest: (isGuest: boolean) => void;
|
|
49
|
-
/** Set error message */
|
|
50
|
-
setError: (error: string | null) => void;
|
|
51
|
-
/** Mark as initialized */
|
|
52
|
-
setInitialized: (initialized: boolean) => void;
|
|
53
|
-
/** Reset to initial state */
|
|
54
|
-
reset: () => void;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// =============================================================================
|
|
58
|
-
// INITIAL STATE
|
|
59
|
-
// =============================================================================
|
|
60
|
-
|
|
61
|
-
const initialState: AuthState = {
|
|
62
|
-
user: null,
|
|
63
|
-
firebaseUser: null,
|
|
64
|
-
loading: true,
|
|
65
|
-
isGuest: false,
|
|
66
|
-
error: null,
|
|
67
|
-
initialized: false,
|
|
12
|
+
import type { AuthState, AuthActions, UserType } from "../../types/auth-store.types";
|
|
13
|
+
import { initialAuthState } from "../../types/auth-store.types";
|
|
14
|
+
import {
|
|
15
|
+
selectUserId,
|
|
16
|
+
selectIsAuthenticated,
|
|
17
|
+
selectIsAnonymous,
|
|
18
|
+
selectUserType,
|
|
19
|
+
selectIsAuthReady,
|
|
20
|
+
} from "./auth.selectors";
|
|
21
|
+
|
|
22
|
+
// Re-export types for convenience
|
|
23
|
+
export type { AuthState, AuthActions, UserType };
|
|
24
|
+
|
|
25
|
+
// Re-export selectors
|
|
26
|
+
export {
|
|
27
|
+
selectUserId,
|
|
28
|
+
selectIsAuthenticated,
|
|
29
|
+
selectIsAnonymous,
|
|
30
|
+
selectUserType,
|
|
31
|
+
selectIsAuthReady,
|
|
68
32
|
};
|
|
69
33
|
|
|
34
|
+
// Re-export listener functions
|
|
35
|
+
export {
|
|
36
|
+
initializeAuthListener,
|
|
37
|
+
resetAuthListener,
|
|
38
|
+
isAuthListenerInitialized,
|
|
39
|
+
} from "./initializeAuthListener";
|
|
40
|
+
|
|
70
41
|
// =============================================================================
|
|
71
42
|
// STORE
|
|
72
43
|
// =============================================================================
|
|
73
44
|
|
|
74
45
|
export const useAuthStore = createStore<AuthState, AuthActions>({
|
|
75
46
|
name: "auth-store",
|
|
76
|
-
initialState,
|
|
47
|
+
initialState: initialAuthState,
|
|
77
48
|
persist: true,
|
|
78
49
|
version: 1,
|
|
79
50
|
partialize: (state) => ({
|
|
@@ -122,56 +93,10 @@ export const useAuthStore = createStore<AuthState, AuthActions>({
|
|
|
122
93
|
|
|
123
94
|
setInitialized: (initialized) => set({ initialized }),
|
|
124
95
|
|
|
125
|
-
reset: () => set(
|
|
96
|
+
reset: () => set(initialAuthState),
|
|
126
97
|
}),
|
|
127
98
|
});
|
|
128
99
|
|
|
129
|
-
// =============================================================================
|
|
130
|
-
// SELECTORS
|
|
131
|
-
// =============================================================================
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get current user ID
|
|
135
|
-
*/
|
|
136
|
-
export const selectUserId = (state: AuthState): string | null => {
|
|
137
|
-
return state.firebaseUser?.uid ?? state.user?.uid ?? null;
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Check if user is authenticated (not guest, not anonymous)
|
|
142
|
-
*/
|
|
143
|
-
export const selectIsAuthenticated = (state: AuthState): boolean => {
|
|
144
|
-
return !!state.user && !state.isGuest && !state.user.isAnonymous;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Check if user is anonymous
|
|
149
|
-
*/
|
|
150
|
-
export const selectIsAnonymous = (state: AuthState): boolean => {
|
|
151
|
-
return state.firebaseUser?.isAnonymous ?? state.user?.isAnonymous ?? false;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Get current user type
|
|
156
|
-
*/
|
|
157
|
-
export const selectUserType = (state: AuthState): UserType => {
|
|
158
|
-
if (!state.firebaseUser && !state.user) {
|
|
159
|
-
return "none";
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const isAnonymous =
|
|
163
|
-
state.firebaseUser?.isAnonymous ?? state.user?.isAnonymous ?? false;
|
|
164
|
-
|
|
165
|
-
return isAnonymous ? "anonymous" : "authenticated";
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Check if auth is ready (initialized and not loading)
|
|
170
|
-
*/
|
|
171
|
-
export const selectIsAuthReady = (state: AuthState): boolean => {
|
|
172
|
-
return state.initialized && !state.loading;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
100
|
// =============================================================================
|
|
176
101
|
// NON-HOOK GETTERS
|
|
177
102
|
// =============================================================================
|
|
@@ -210,64 +135,3 @@ export function getIsGuest(): boolean {
|
|
|
210
135
|
export function getIsAnonymous(): boolean {
|
|
211
136
|
return selectIsAnonymous(useAuthStore.getState());
|
|
212
137
|
}
|
|
213
|
-
|
|
214
|
-
// =============================================================================
|
|
215
|
-
// LISTENER
|
|
216
|
-
// =============================================================================
|
|
217
|
-
|
|
218
|
-
let listenerInitialized = false;
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Initialize Firebase auth listener
|
|
222
|
-
* Call once in app root, returns unsubscribe function
|
|
223
|
-
*/
|
|
224
|
-
export function initializeAuthListener(): () => void {
|
|
225
|
-
if (listenerInitialized) {
|
|
226
|
-
return () => {};
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const auth = getFirebaseAuth();
|
|
230
|
-
const store = useAuthStore.getState();
|
|
231
|
-
|
|
232
|
-
if (!auth) {
|
|
233
|
-
store.setLoading(false);
|
|
234
|
-
store.setInitialized(true);
|
|
235
|
-
return () => {};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const service = getAuthService();
|
|
239
|
-
if (service) {
|
|
240
|
-
const isGuest = service.getIsGuestMode();
|
|
241
|
-
if (isGuest) {
|
|
242
|
-
store.setIsGuest(true);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
listenerInitialized = true;
|
|
247
|
-
|
|
248
|
-
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
|
249
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
250
|
-
// eslint-disable-next-line no-console
|
|
251
|
-
console.log("[authStore] Auth state changed:", user?.uid ?? "null");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
store.setFirebaseUser(user);
|
|
255
|
-
store.setInitialized(true);
|
|
256
|
-
|
|
257
|
-
if (user && !user.isAnonymous && store.isGuest) {
|
|
258
|
-
store.setIsGuest(false);
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
return () => {
|
|
263
|
-
unsubscribe();
|
|
264
|
-
listenerInitialized = false;
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Reset listener state (for testing)
|
|
270
|
-
*/
|
|
271
|
-
export function resetAuthListener(): void {
|
|
272
|
-
listenerInitialized = false;
|
|
273
|
-
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Listener Initialization
|
|
3
|
+
* Sets up Firebase auth state listener with optional auto anonymous sign-in
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { onAuthStateChanged } from "firebase/auth";
|
|
7
|
+
import {
|
|
8
|
+
getFirebaseAuth,
|
|
9
|
+
anonymousAuthService,
|
|
10
|
+
} from "@umituz/react-native-firebase";
|
|
11
|
+
import { useAuthStore } from "./authStore";
|
|
12
|
+
import { getAuthService } from "../../infrastructure/services/AuthService";
|
|
13
|
+
import type { AuthListenerOptions } from "../../types/auth-store.types";
|
|
14
|
+
|
|
15
|
+
declare const __DEV__: boolean;
|
|
16
|
+
|
|
17
|
+
let listenerInitialized = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize Firebase auth listener
|
|
21
|
+
* Call once in app root, returns unsubscribe function
|
|
22
|
+
*
|
|
23
|
+
* @param options - Configuration options
|
|
24
|
+
* @param options.autoAnonymousSignIn - Enable auto anonymous sign-in (default: true)
|
|
25
|
+
* @param options.onAuthStateChange - Callback when auth state changes
|
|
26
|
+
*/
|
|
27
|
+
export function initializeAuthListener(
|
|
28
|
+
options: AuthListenerOptions = {}
|
|
29
|
+
): () => void {
|
|
30
|
+
const { autoAnonymousSignIn = true, onAuthStateChange } = options;
|
|
31
|
+
|
|
32
|
+
if (listenerInitialized) {
|
|
33
|
+
return () => {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const auth = getFirebaseAuth();
|
|
37
|
+
const store = useAuthStore.getState();
|
|
38
|
+
|
|
39
|
+
if (!auth) {
|
|
40
|
+
store.setLoading(false);
|
|
41
|
+
store.setInitialized(true);
|
|
42
|
+
return () => {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const service = getAuthService();
|
|
46
|
+
if (service) {
|
|
47
|
+
const isGuest = service.getIsGuestMode();
|
|
48
|
+
if (isGuest) {
|
|
49
|
+
store.setIsGuest(true);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
listenerInitialized = true;
|
|
54
|
+
|
|
55
|
+
const unsubscribe = onAuthStateChanged(auth, (user) => {
|
|
56
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
58
|
+
console.log("[authStore] Auth state changed:", user?.uid ?? "null");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Auto sign-in anonymously if no user and autoAnonymousSignIn is enabled
|
|
62
|
+
if (!user && autoAnonymousSignIn) {
|
|
63
|
+
void (async () => {
|
|
64
|
+
try {
|
|
65
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.log("[authStore] Auto signing in anonymously...");
|
|
68
|
+
}
|
|
69
|
+
await anonymousAuthService.signInAnonymously(auth);
|
|
70
|
+
// The listener will be called again with the new anonymous user
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.warn("[authStore] Auto anonymous sign-in failed:", error);
|
|
75
|
+
}
|
|
76
|
+
// Continue with null user if anonymous sign-in fails
|
|
77
|
+
store.setFirebaseUser(null);
|
|
78
|
+
store.setInitialized(true);
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
store.setFirebaseUser(user);
|
|
85
|
+
store.setInitialized(true);
|
|
86
|
+
|
|
87
|
+
if (user && !user.isAnonymous && store.isGuest) {
|
|
88
|
+
store.setIsGuest(false);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Call optional callback
|
|
92
|
+
onAuthStateChange?.(user);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
unsubscribe();
|
|
97
|
+
listenerInitialized = false;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reset listener state (for testing)
|
|
103
|
+
*/
|
|
104
|
+
export function resetAuthListener(): void {
|
|
105
|
+
listenerInitialized = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if listener is initialized
|
|
110
|
+
*/
|
|
111
|
+
export function isAuthListenerInitialized(): boolean {
|
|
112
|
+
return listenerInitialized;
|
|
113
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Store Types
|
|
3
|
+
* Type definitions for auth state management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { User } from "firebase/auth";
|
|
7
|
+
import type { AuthUser } from "../domain/entities/AuthUser";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* User type classification
|
|
11
|
+
*/
|
|
12
|
+
export type UserType = "authenticated" | "anonymous" | "none";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auth state interface
|
|
16
|
+
*/
|
|
17
|
+
export interface AuthState {
|
|
18
|
+
/** Mapped AuthUser (null if not authenticated) */
|
|
19
|
+
user: AuthUser | null;
|
|
20
|
+
/** Raw Firebase user reference */
|
|
21
|
+
firebaseUser: User | null;
|
|
22
|
+
/** Loading state during auth operations */
|
|
23
|
+
loading: boolean;
|
|
24
|
+
/** Guest mode (user skipped authentication) */
|
|
25
|
+
isGuest: boolean;
|
|
26
|
+
/** Error message from last auth operation */
|
|
27
|
+
error: string | null;
|
|
28
|
+
/** Whether auth listener has initialized */
|
|
29
|
+
initialized: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Auth store actions interface
|
|
34
|
+
*/
|
|
35
|
+
export interface AuthActions {
|
|
36
|
+
/** Update user from Firebase listener */
|
|
37
|
+
setFirebaseUser: (user: User | null) => void;
|
|
38
|
+
/** Set loading state */
|
|
39
|
+
setLoading: (loading: boolean) => void;
|
|
40
|
+
/** Set guest mode */
|
|
41
|
+
setIsGuest: (isGuest: boolean) => void;
|
|
42
|
+
/** Set error message */
|
|
43
|
+
setError: (error: string | null) => void;
|
|
44
|
+
/** Mark as initialized */
|
|
45
|
+
setInitialized: (initialized: boolean) => void;
|
|
46
|
+
/** Reset to initial state */
|
|
47
|
+
reset: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initial auth state
|
|
52
|
+
*/
|
|
53
|
+
export const initialAuthState: AuthState = {
|
|
54
|
+
user: null,
|
|
55
|
+
firebaseUser: null,
|
|
56
|
+
loading: true,
|
|
57
|
+
isGuest: false,
|
|
58
|
+
error: null,
|
|
59
|
+
initialized: false,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Auth listener initialization options
|
|
64
|
+
*/
|
|
65
|
+
export interface AuthListenerOptions {
|
|
66
|
+
/** Enable auto anonymous sign-in when no user is logged in */
|
|
67
|
+
autoAnonymousSignIn?: boolean;
|
|
68
|
+
/** Callback when auth state changes */
|
|
69
|
+
onAuthStateChange?: (user: User | null) => void;
|
|
70
|
+
}
|