@umituz/react-native-auth 1.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/LICENSE +22 -0
- package/README.md +216 -0
- package/package.json +55 -0
- package/src/application/ports/IAuthService.ts +55 -0
- package/src/domain/errors/AuthError.ts +85 -0
- package/src/domain/value-objects/AuthConfig.ts +32 -0
- package/src/index.ts +62 -0
- package/src/infrastructure/services/AuthService.ts +353 -0
- package/src/presentation/hooks/useAuth.ts +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ümit UZ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# @umituz/react-native-auth
|
|
2
|
+
|
|
3
|
+
Firebase Authentication wrapper for React Native apps - Secure, type-safe, and production-ready.
|
|
4
|
+
|
|
5
|
+
Built with **SOLID**, **DRY**, and **KISS** principles.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @umituz/react-native-auth
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Peer Dependencies
|
|
14
|
+
|
|
15
|
+
- `firebase` >= 11.0.0
|
|
16
|
+
- `react` >= 18.2.0
|
|
17
|
+
- `react-native` >= 0.74.0
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- ✅ Domain-Driven Design (DDD) architecture
|
|
22
|
+
- ✅ SOLID principles (Single Responsibility, Open/Closed, etc.)
|
|
23
|
+
- ✅ DRY (Don't Repeat Yourself)
|
|
24
|
+
- ✅ KISS (Keep It Simple, Stupid)
|
|
25
|
+
- ✅ **Security**: Password validation, email validation, error handling
|
|
26
|
+
- ✅ Type-safe operations
|
|
27
|
+
- ✅ Guest mode support
|
|
28
|
+
- ✅ React hooks for easy integration
|
|
29
|
+
- ✅ Works with Expo and React Native CLI
|
|
30
|
+
|
|
31
|
+
## Important: Security First
|
|
32
|
+
|
|
33
|
+
**This package prioritizes security:**
|
|
34
|
+
|
|
35
|
+
- Password strength validation
|
|
36
|
+
- Email format validation
|
|
37
|
+
- Secure error handling (no sensitive data exposure)
|
|
38
|
+
- Firebase Auth best practices
|
|
39
|
+
- Guest mode support for offline-first apps
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### 1. Initialize Auth Service
|
|
44
|
+
|
|
45
|
+
Initialize the service early in your app (e.g., in `App.tsx`):
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { initializeAuthService } from '@umituz/react-native-auth';
|
|
49
|
+
import { getFirebaseAuth } from '@umituz/react-native-firebase';
|
|
50
|
+
|
|
51
|
+
// Initialize Firebase first (using @umituz/react-native-firebase)
|
|
52
|
+
const auth = getFirebaseAuth();
|
|
53
|
+
|
|
54
|
+
// Initialize auth service
|
|
55
|
+
initializeAuthService(auth, {
|
|
56
|
+
minPasswordLength: 6,
|
|
57
|
+
requireUppercase: false,
|
|
58
|
+
requireLowercase: false,
|
|
59
|
+
requireNumbers: false,
|
|
60
|
+
requireSpecialChars: false,
|
|
61
|
+
onUserCreated: async (user) => {
|
|
62
|
+
// Optional: Create user profile in your database
|
|
63
|
+
console.log('User created:', user.uid);
|
|
64
|
+
},
|
|
65
|
+
onSignOut: async () => {
|
|
66
|
+
// Optional: Cleanup on sign out
|
|
67
|
+
console.log('User signed out');
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Use Auth Hook in Components
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { useAuth } from '@umituz/react-native-auth';
|
|
76
|
+
|
|
77
|
+
function LoginScreen() {
|
|
78
|
+
const { user, isAuthenticated, isGuest, loading, signIn, signUp, signOut, continueAsGuest } = useAuth();
|
|
79
|
+
|
|
80
|
+
if (loading) {
|
|
81
|
+
return <LoadingSpinner />;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isAuthenticated) {
|
|
85
|
+
return <HomeScreen user={user} />;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isGuest) {
|
|
89
|
+
return <GuestHomeScreen />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return <LoginForm onSignIn={signIn} onSignUp={signUp} onContinueAsGuest={continueAsGuest} />;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 3. Sign Up
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { getAuthService } from '@umituz/react-native-auth';
|
|
100
|
+
|
|
101
|
+
const authService = getAuthService();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const user = await authService.signUp({
|
|
105
|
+
email: 'user@example.com',
|
|
106
|
+
password: 'securepassword123',
|
|
107
|
+
displayName: 'John Doe',
|
|
108
|
+
});
|
|
109
|
+
console.log('User signed up:', user.uid);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof AuthEmailAlreadyInUseError) {
|
|
112
|
+
console.error('Email already in use');
|
|
113
|
+
} else if (error instanceof AuthWeakPasswordError) {
|
|
114
|
+
console.error('Password is too weak');
|
|
115
|
+
} else {
|
|
116
|
+
console.error('Sign up failed:', error.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4. Sign In
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
try {
|
|
125
|
+
const user = await authService.signIn({
|
|
126
|
+
email: 'user@example.com',
|
|
127
|
+
password: 'securepassword123',
|
|
128
|
+
});
|
|
129
|
+
console.log('User signed in:', user.uid);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof AuthWrongPasswordError) {
|
|
132
|
+
console.error('Wrong password');
|
|
133
|
+
} else if (error instanceof AuthUserNotFoundError) {
|
|
134
|
+
console.error('User not found');
|
|
135
|
+
} else {
|
|
136
|
+
console.error('Sign in failed:', error.message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 5. Sign Out
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
await authService.signOut();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 6. Guest Mode
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
await authService.setGuestMode();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## API
|
|
154
|
+
|
|
155
|
+
### Functions
|
|
156
|
+
|
|
157
|
+
- `initializeAuthService(auth, config?)`: Initialize auth service with Firebase Auth instance
|
|
158
|
+
- `getAuthService()`: Get auth service instance (throws if not initialized)
|
|
159
|
+
- `resetAuthService()`: Reset service instance (useful for testing)
|
|
160
|
+
|
|
161
|
+
### Hook
|
|
162
|
+
|
|
163
|
+
- `useAuth()`: React hook for authentication state management
|
|
164
|
+
|
|
165
|
+
### Types
|
|
166
|
+
|
|
167
|
+
- `AuthConfig`: Configuration interface
|
|
168
|
+
- `SignUpParams`: Sign up parameters
|
|
169
|
+
- `SignInParams`: Sign in parameters
|
|
170
|
+
- `UseAuthResult`: Hook return type
|
|
171
|
+
|
|
172
|
+
### Errors
|
|
173
|
+
|
|
174
|
+
- `AuthError`: Base error class
|
|
175
|
+
- `AuthInitializationError`: Initialization errors
|
|
176
|
+
- `AuthConfigurationError`: Configuration errors
|
|
177
|
+
- `AuthValidationError`: Validation errors
|
|
178
|
+
- `AuthNetworkError`: Network errors
|
|
179
|
+
- `AuthUserNotFoundError`: User not found
|
|
180
|
+
- `AuthWrongPasswordError`: Wrong password
|
|
181
|
+
- `AuthEmailAlreadyInUseError`: Email already in use
|
|
182
|
+
- `AuthWeakPasswordError`: Weak password
|
|
183
|
+
- `AuthInvalidEmailError`: Invalid email
|
|
184
|
+
|
|
185
|
+
## Security Best Practices
|
|
186
|
+
|
|
187
|
+
1. **Password Validation**: Configure password requirements based on your app's security needs
|
|
188
|
+
2. **Error Handling**: Always handle errors gracefully without exposing sensitive information
|
|
189
|
+
3. **Guest Mode**: Use guest mode for offline-first apps that don't require authentication
|
|
190
|
+
4. **User Callbacks**: Use `onUserCreated` and `onSignOut` callbacks for app-specific logic
|
|
191
|
+
|
|
192
|
+
## Integration with @umituz/react-native-firebase
|
|
193
|
+
|
|
194
|
+
This package works seamlessly with `@umituz/react-native-firebase`:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { initializeFirebase, getFirebaseAuth } from '@umituz/react-native-firebase';
|
|
198
|
+
import { initializeAuthService } from '@umituz/react-native-auth';
|
|
199
|
+
|
|
200
|
+
// Initialize Firebase
|
|
201
|
+
const config = {
|
|
202
|
+
apiKey: 'your-api-key',
|
|
203
|
+
authDomain: 'your-project.firebaseapp.com',
|
|
204
|
+
projectId: 'your-project-id',
|
|
205
|
+
};
|
|
206
|
+
initializeFirebase(config);
|
|
207
|
+
|
|
208
|
+
// Initialize Auth Service
|
|
209
|
+
const auth = getFirebaseAuth();
|
|
210
|
+
initializeAuthService(auth);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
216
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umituz/react-native-auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Firebase Authentication wrapper for React Native apps - Secure, type-safe, and production-ready",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit",
|
|
9
|
+
"lint": "tsc --noEmit",
|
|
10
|
+
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
11
|
+
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
12
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react-native",
|
|
16
|
+
"firebase",
|
|
17
|
+
"authentication",
|
|
18
|
+
"auth",
|
|
19
|
+
"security",
|
|
20
|
+
"ddd",
|
|
21
|
+
"domain-driven-design",
|
|
22
|
+
"type-safe",
|
|
23
|
+
"solid",
|
|
24
|
+
"dry",
|
|
25
|
+
"kiss"
|
|
26
|
+
],
|
|
27
|
+
"author": "Ümit UZ <umit@umituz.com>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/umituz/react-native-auth.git"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"firebase": ">=11.0.0",
|
|
35
|
+
"react": ">=18.2.0",
|
|
36
|
+
"react-native": ">=0.74.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"firebase": "^11.10.0",
|
|
40
|
+
"@types/react": "^18.2.45",
|
|
41
|
+
"@types/react-native": "^0.73.0",
|
|
42
|
+
"react": "^18.2.0",
|
|
43
|
+
"react-native": "^0.74.0",
|
|
44
|
+
"typescript": "^5.3.3"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"src",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Service Interface
|
|
3
|
+
* Port for authentication operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { User } from "firebase/auth";
|
|
7
|
+
|
|
8
|
+
export interface SignUpParams {
|
|
9
|
+
email: string;
|
|
10
|
+
password: string;
|
|
11
|
+
displayName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SignInParams {
|
|
15
|
+
email: string;
|
|
16
|
+
password: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IAuthService {
|
|
20
|
+
/**
|
|
21
|
+
* Sign up a new user
|
|
22
|
+
*/
|
|
23
|
+
signUp(params: SignUpParams): Promise<User>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sign in an existing user
|
|
27
|
+
*/
|
|
28
|
+
signIn(params: SignInParams): Promise<User>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sign out current user
|
|
32
|
+
*/
|
|
33
|
+
signOut(): Promise<void>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Set guest mode (no authentication)
|
|
37
|
+
*/
|
|
38
|
+
setGuestMode(): Promise<void>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get current authenticated user
|
|
42
|
+
*/
|
|
43
|
+
getCurrentUser(): User | null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Subscribe to auth state changes
|
|
47
|
+
*/
|
|
48
|
+
onAuthStateChange(callback: (user: User | null) => void): () => void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if auth is initialized
|
|
52
|
+
*/
|
|
53
|
+
isInitialized(): boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Error Types
|
|
3
|
+
* Domain-specific error classes for authentication operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class AuthError extends Error {
|
|
7
|
+
constructor(message: string, public code?: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "AuthError";
|
|
10
|
+
Object.setPrototypeOf(this, AuthError.prototype);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class AuthInitializationError extends AuthError {
|
|
15
|
+
constructor(message: string = "Firebase Auth is not initialized") {
|
|
16
|
+
super(message, "AUTH_NOT_INITIALIZED");
|
|
17
|
+
this.name = "AuthInitializationError";
|
|
18
|
+
Object.setPrototypeOf(this, AuthInitializationError.prototype);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class AuthConfigurationError extends AuthError {
|
|
23
|
+
constructor(message: string = "Invalid auth configuration") {
|
|
24
|
+
super(message, "AUTH_CONFIG_ERROR");
|
|
25
|
+
this.name = "AuthConfigurationError";
|
|
26
|
+
Object.setPrototypeOf(this, AuthConfigurationError.prototype);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class AuthValidationError extends AuthError {
|
|
31
|
+
constructor(message: string, public field?: string) {
|
|
32
|
+
super(message, "AUTH_VALIDATION_ERROR");
|
|
33
|
+
this.name = "AuthValidationError";
|
|
34
|
+
Object.setPrototypeOf(this, AuthValidationError.prototype);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class AuthNetworkError extends AuthError {
|
|
39
|
+
constructor(message: string = "Network error during authentication") {
|
|
40
|
+
super(message, "AUTH_NETWORK_ERROR");
|
|
41
|
+
this.name = "AuthNetworkError";
|
|
42
|
+
Object.setPrototypeOf(this, AuthNetworkError.prototype);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class AuthUserNotFoundError extends AuthError {
|
|
47
|
+
constructor(message: string = "User not found") {
|
|
48
|
+
super(message, "AUTH_USER_NOT_FOUND");
|
|
49
|
+
this.name = "AuthUserNotFoundError";
|
|
50
|
+
Object.setPrototypeOf(this, AuthUserNotFoundError.prototype);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class AuthWrongPasswordError extends AuthError {
|
|
55
|
+
constructor(message: string = "Wrong password") {
|
|
56
|
+
super(message, "AUTH_WRONG_PASSWORD");
|
|
57
|
+
this.name = "AuthWrongPasswordError";
|
|
58
|
+
Object.setPrototypeOf(this, AuthWrongPasswordError.prototype);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class AuthEmailAlreadyInUseError extends AuthError {
|
|
63
|
+
constructor(message: string = "Email already in use") {
|
|
64
|
+
super(message, "AUTH_EMAIL_ALREADY_IN_USE");
|
|
65
|
+
this.name = "AuthEmailAlreadyInUseError";
|
|
66
|
+
Object.setPrototypeOf(this, AuthEmailAlreadyInUseError.prototype);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class AuthWeakPasswordError extends AuthError {
|
|
71
|
+
constructor(message: string = "Password is too weak") {
|
|
72
|
+
super(message, "AUTH_WEAK_PASSWORD");
|
|
73
|
+
this.name = "AuthWeakPasswordError";
|
|
74
|
+
Object.setPrototypeOf(this, AuthWeakPasswordError.prototype);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class AuthInvalidEmailError extends AuthError {
|
|
79
|
+
constructor(message: string = "Invalid email address") {
|
|
80
|
+
super(message, "AUTH_INVALID_EMAIL");
|
|
81
|
+
this.name = "AuthInvalidEmailError";
|
|
82
|
+
Object.setPrototypeOf(this, AuthInvalidEmailError.prototype);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Configuration Value Object
|
|
3
|
+
* Validates and stores authentication configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface AuthConfig {
|
|
7
|
+
/** Minimum password length (default: 6) */
|
|
8
|
+
minPasswordLength?: number;
|
|
9
|
+
/** Require uppercase letters in password */
|
|
10
|
+
requireUppercase?: boolean;
|
|
11
|
+
/** Require lowercase letters in password */
|
|
12
|
+
requireLowercase?: boolean;
|
|
13
|
+
/** Require numbers in password */
|
|
14
|
+
requireNumbers?: boolean;
|
|
15
|
+
/** Require special characters in password */
|
|
16
|
+
requireSpecialChars?: boolean;
|
|
17
|
+
/** Callback for user profile creation after signup */
|
|
18
|
+
onUserCreated?: (user: any) => Promise<void> | void;
|
|
19
|
+
/** Callback for user profile update */
|
|
20
|
+
onUserUpdated?: (user: any) => Promise<void> | void;
|
|
21
|
+
/** Callback for sign out cleanup */
|
|
22
|
+
onSignOut?: () => Promise<void> | void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_AUTH_CONFIG: Required<Omit<AuthConfig, 'onUserCreated' | 'onUserUpdated' | 'onSignOut'>> = {
|
|
26
|
+
minPasswordLength: 6,
|
|
27
|
+
requireUppercase: false,
|
|
28
|
+
requireLowercase: false,
|
|
29
|
+
requireNumbers: false,
|
|
30
|
+
requireSpecialChars: false,
|
|
31
|
+
};
|
|
32
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Auth - Public API
|
|
3
|
+
*
|
|
4
|
+
* Domain-Driven Design (DDD) Architecture
|
|
5
|
+
*
|
|
6
|
+
* This is the SINGLE SOURCE OF TRUTH for all Auth operations.
|
|
7
|
+
* ALL imports from the Auth package MUST go through this file.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* - domain: Entities, value objects, errors (business logic)
|
|
11
|
+
* - application: Ports (interfaces)
|
|
12
|
+
* - infrastructure: Auth service implementation
|
|
13
|
+
* - presentation: Hooks (React integration)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* import { initializeAuthService, useAuth } from '@umituz/react-native-auth';
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// DOMAIN LAYER - Business Logic
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
AuthError,
|
|
25
|
+
AuthInitializationError,
|
|
26
|
+
AuthConfigurationError,
|
|
27
|
+
AuthValidationError,
|
|
28
|
+
AuthNetworkError,
|
|
29
|
+
AuthUserNotFoundError,
|
|
30
|
+
AuthWrongPasswordError,
|
|
31
|
+
AuthEmailAlreadyInUseError,
|
|
32
|
+
AuthWeakPasswordError,
|
|
33
|
+
AuthInvalidEmailError,
|
|
34
|
+
} from './domain/errors/AuthError';
|
|
35
|
+
|
|
36
|
+
export type { AuthConfig } from './domain/value-objects/AuthConfig';
|
|
37
|
+
export { DEFAULT_AUTH_CONFIG } from './domain/value-objects/AuthConfig';
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// APPLICATION LAYER - Ports
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
export type { IAuthService, SignUpParams, SignInParams } from './application/ports/IAuthService';
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// INFRASTRUCTURE LAYER - Implementation
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
AuthService,
|
|
51
|
+
initializeAuthService,
|
|
52
|
+
getAuthService,
|
|
53
|
+
resetAuthService,
|
|
54
|
+
} from './infrastructure/services/AuthService';
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// PRESENTATION LAYER - Hooks
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
export { useAuth } from './presentation/hooks/useAuth';
|
|
61
|
+
export type { UseAuthResult } from './presentation/hooks/useAuth';
|
|
62
|
+
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Service Implementation
|
|
3
|
+
* Secure Firebase Authentication wrapper
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createUserWithEmailAndPassword,
|
|
8
|
+
signInWithEmailAndPassword,
|
|
9
|
+
signOut as firebaseSignOut,
|
|
10
|
+
onAuthStateChanged,
|
|
11
|
+
updateProfile,
|
|
12
|
+
type User,
|
|
13
|
+
type Auth,
|
|
14
|
+
} from "firebase/auth";
|
|
15
|
+
import type { IAuthService, SignUpParams, SignInParams } from "../application/ports/IAuthService";
|
|
16
|
+
import {
|
|
17
|
+
AuthInitializationError,
|
|
18
|
+
AuthValidationError,
|
|
19
|
+
AuthWeakPasswordError,
|
|
20
|
+
AuthInvalidEmailError,
|
|
21
|
+
AuthEmailAlreadyInUseError,
|
|
22
|
+
AuthWrongPasswordError,
|
|
23
|
+
AuthUserNotFoundError,
|
|
24
|
+
AuthNetworkError,
|
|
25
|
+
} from "../domain/errors/AuthError";
|
|
26
|
+
import type { AuthConfig } from "../domain/value-objects/AuthConfig";
|
|
27
|
+
import { DEFAULT_AUTH_CONFIG } from "../domain/value-objects/AuthConfig";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate email format
|
|
31
|
+
*/
|
|
32
|
+
function validateEmail(email: string): boolean {
|
|
33
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
34
|
+
return emailRegex.test(email.trim());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate password strength
|
|
39
|
+
*/
|
|
40
|
+
function validatePassword(
|
|
41
|
+
password: string,
|
|
42
|
+
config: Required<Omit<AuthConfig, "onUserCreated" | "onUserUpdated" | "onSignOut">>
|
|
43
|
+
): { valid: boolean; error?: string } {
|
|
44
|
+
if (password.length < config.minPasswordLength) {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
error: `Password must be at least ${config.minPasswordLength} characters long`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (config.requireUppercase && !/[A-Z]/.test(password)) {
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
error: "Password must contain at least one uppercase letter",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (config.requireLowercase && !/[a-z]/.test(password)) {
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
error: "Password must contain at least one lowercase letter",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (config.requireNumbers && !/[0-9]/.test(password)) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
error: "Password must contain at least one number",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (config.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
|
73
|
+
return {
|
|
74
|
+
valid: false,
|
|
75
|
+
error: "Password must contain at least one special character",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { valid: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Map Firebase Auth errors to domain errors
|
|
84
|
+
*/
|
|
85
|
+
function mapFirebaseAuthError(error: any): Error {
|
|
86
|
+
const code = error?.code || "";
|
|
87
|
+
const message = error?.message || "Authentication failed";
|
|
88
|
+
|
|
89
|
+
// Firebase Auth error codes
|
|
90
|
+
if (code === "auth/email-already-in-use") {
|
|
91
|
+
return new AuthEmailAlreadyInUseError();
|
|
92
|
+
}
|
|
93
|
+
if (code === "auth/invalid-email") {
|
|
94
|
+
return new AuthInvalidEmailError();
|
|
95
|
+
}
|
|
96
|
+
if (code === "auth/operation-not-allowed") {
|
|
97
|
+
return new AuthConfigurationError("Email/password authentication is not enabled");
|
|
98
|
+
}
|
|
99
|
+
if (code === "auth/weak-password") {
|
|
100
|
+
return new AuthWeakPasswordError();
|
|
101
|
+
}
|
|
102
|
+
if (code === "auth/user-disabled") {
|
|
103
|
+
return new AuthError("User account has been disabled", "AUTH_USER_DISABLED");
|
|
104
|
+
}
|
|
105
|
+
if (code === "auth/user-not-found") {
|
|
106
|
+
return new AuthUserNotFoundError();
|
|
107
|
+
}
|
|
108
|
+
if (code === "auth/wrong-password") {
|
|
109
|
+
return new AuthWrongPasswordError();
|
|
110
|
+
}
|
|
111
|
+
if (code === "auth/network-request-failed") {
|
|
112
|
+
return new AuthNetworkError();
|
|
113
|
+
}
|
|
114
|
+
if (code === "auth/too-many-requests") {
|
|
115
|
+
return new AuthError("Too many requests. Please try again later.", "AUTH_TOO_MANY_REQUESTS");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return new AuthError(message, code);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class AuthService implements IAuthService {
|
|
122
|
+
private auth: Auth | null = null;
|
|
123
|
+
private config: AuthConfig;
|
|
124
|
+
private isGuestMode: boolean = false;
|
|
125
|
+
|
|
126
|
+
constructor(config: AuthConfig = {}) {
|
|
127
|
+
this.config = { ...DEFAULT_AUTH_CONFIG, ...config };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Initialize auth service with Firebase Auth instance
|
|
132
|
+
* Must be called before using any auth methods
|
|
133
|
+
*/
|
|
134
|
+
initialize(auth: Auth): void {
|
|
135
|
+
if (!auth) {
|
|
136
|
+
throw new AuthInitializationError("Auth instance is required");
|
|
137
|
+
}
|
|
138
|
+
this.auth = auth;
|
|
139
|
+
this.isGuestMode = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if auth is initialized
|
|
144
|
+
*/
|
|
145
|
+
isInitialized(): boolean {
|
|
146
|
+
return this.auth !== null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private getAuth(): Auth {
|
|
150
|
+
if (!this.auth) {
|
|
151
|
+
throw new AuthInitializationError();
|
|
152
|
+
}
|
|
153
|
+
return this.auth;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sign up a new user
|
|
158
|
+
*/
|
|
159
|
+
async signUp(params: SignUpParams): Promise<User> {
|
|
160
|
+
const auth = this.getAuth();
|
|
161
|
+
|
|
162
|
+
// Validate email
|
|
163
|
+
if (!params.email || !validateEmail(params.email)) {
|
|
164
|
+
throw new AuthInvalidEmailError();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validate password
|
|
168
|
+
const passwordValidation = validatePassword(params.password, this.config as any);
|
|
169
|
+
if (!passwordValidation.valid) {
|
|
170
|
+
throw new AuthWeakPasswordError(passwordValidation.error);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// Create user
|
|
175
|
+
const userCredential = await createUserWithEmailAndPassword(
|
|
176
|
+
auth,
|
|
177
|
+
params.email.trim(),
|
|
178
|
+
params.password
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Update display name if provided
|
|
182
|
+
if (params.displayName && userCredential.user) {
|
|
183
|
+
try {
|
|
184
|
+
await updateProfile(userCredential.user, {
|
|
185
|
+
displayName: params.displayName.trim(),
|
|
186
|
+
});
|
|
187
|
+
} catch (updateError) {
|
|
188
|
+
// Don't fail signup if display name update fails
|
|
189
|
+
// User can still use the app
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Call user created callback if provided
|
|
194
|
+
if (this.config.onUserCreated) {
|
|
195
|
+
try {
|
|
196
|
+
await this.config.onUserCreated(userCredential.user);
|
|
197
|
+
} catch (callbackError) {
|
|
198
|
+
// Don't fail signup if callback fails
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return userCredential.user;
|
|
203
|
+
} catch (error: any) {
|
|
204
|
+
throw mapFirebaseAuthError(error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sign in an existing user
|
|
210
|
+
*/
|
|
211
|
+
async signIn(params: SignInParams): Promise<User> {
|
|
212
|
+
const auth = this.getAuth();
|
|
213
|
+
|
|
214
|
+
// Validate email
|
|
215
|
+
if (!params.email || !validateEmail(params.email)) {
|
|
216
|
+
throw new AuthInvalidEmailError();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Validate password
|
|
220
|
+
if (!params.password || params.password.length === 0) {
|
|
221
|
+
throw new AuthValidationError("Password is required", "password");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const userCredential = await signInWithEmailAndPassword(
|
|
226
|
+
auth,
|
|
227
|
+
params.email.trim(),
|
|
228
|
+
params.password
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
this.isGuestMode = false;
|
|
232
|
+
return userCredential.user;
|
|
233
|
+
} catch (error: any) {
|
|
234
|
+
throw mapFirebaseAuthError(error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Sign out current user
|
|
240
|
+
*/
|
|
241
|
+
async signOut(): Promise<void> {
|
|
242
|
+
const auth = this.getAuth();
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await firebaseSignOut(auth);
|
|
246
|
+
this.isGuestMode = false;
|
|
247
|
+
|
|
248
|
+
// Call sign out callback if provided
|
|
249
|
+
if (this.config.onSignOut) {
|
|
250
|
+
try {
|
|
251
|
+
await this.config.onSignOut();
|
|
252
|
+
} catch (callbackError) {
|
|
253
|
+
// Don't fail signout if callback fails
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch (error: any) {
|
|
257
|
+
throw mapFirebaseAuthError(error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Set guest mode (no authentication)
|
|
263
|
+
*/
|
|
264
|
+
async setGuestMode(): Promise<void> {
|
|
265
|
+
const auth = this.getAuth();
|
|
266
|
+
|
|
267
|
+
// Sign out from Firebase if logged in
|
|
268
|
+
if (auth.currentUser) {
|
|
269
|
+
try {
|
|
270
|
+
await firebaseSignOut(auth);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// Ignore sign out errors when switching to guest mode
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.isGuestMode = true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get current authenticated user
|
|
281
|
+
*/
|
|
282
|
+
getCurrentUser(): User | null {
|
|
283
|
+
if (!this.auth) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return this.auth.currentUser;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Check if user is in guest mode
|
|
291
|
+
*/
|
|
292
|
+
getIsGuestMode(): boolean {
|
|
293
|
+
return this.isGuestMode;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Subscribe to auth state changes
|
|
298
|
+
*/
|
|
299
|
+
onAuthStateChange(callback: (user: User | null) => void): () => void {
|
|
300
|
+
const auth = this.getAuth();
|
|
301
|
+
|
|
302
|
+
return onAuthStateChanged(auth, (user) => {
|
|
303
|
+
// Don't update if in guest mode
|
|
304
|
+
if (!this.isGuestMode) {
|
|
305
|
+
callback(user);
|
|
306
|
+
} else {
|
|
307
|
+
callback(null);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Singleton instance
|
|
315
|
+
* Apps should use initializeAuthService() to set up with their Firebase Auth instance
|
|
316
|
+
*/
|
|
317
|
+
let authServiceInstance: AuthService | null = null;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Initialize auth service with Firebase Auth instance
|
|
321
|
+
* Must be called before using any auth methods
|
|
322
|
+
*/
|
|
323
|
+
export function initializeAuthService(
|
|
324
|
+
auth: Auth,
|
|
325
|
+
config?: AuthConfig
|
|
326
|
+
): AuthService {
|
|
327
|
+
if (!authServiceInstance) {
|
|
328
|
+
authServiceInstance = new AuthService(config);
|
|
329
|
+
}
|
|
330
|
+
authServiceInstance.initialize(auth);
|
|
331
|
+
return authServiceInstance;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get auth service instance
|
|
336
|
+
* @throws {AuthInitializationError} If service is not initialized
|
|
337
|
+
*/
|
|
338
|
+
export function getAuthService(): AuthService {
|
|
339
|
+
if (!authServiceInstance || !authServiceInstance.isInitialized()) {
|
|
340
|
+
throw new AuthInitializationError(
|
|
341
|
+
"Auth service is not initialized. Call initializeAuthService() first."
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return authServiceInstance;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Reset auth service (useful for testing)
|
|
349
|
+
*/
|
|
350
|
+
export function resetAuthService(): void {
|
|
351
|
+
authServiceInstance = null;
|
|
352
|
+
}
|
|
353
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAuth Hook
|
|
3
|
+
* React hook for authentication state management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useState, useCallback } from "react";
|
|
7
|
+
import type { User } from "firebase/auth";
|
|
8
|
+
import { getAuthService } from "../infrastructure/services/AuthService";
|
|
9
|
+
|
|
10
|
+
export interface UseAuthResult {
|
|
11
|
+
/** Current authenticated user */
|
|
12
|
+
user: User | null;
|
|
13
|
+
/** Whether auth state is loading */
|
|
14
|
+
loading: boolean;
|
|
15
|
+
/** Whether user is in guest mode */
|
|
16
|
+
isGuest: boolean;
|
|
17
|
+
/** Whether user is authenticated */
|
|
18
|
+
isAuthenticated: boolean;
|
|
19
|
+
/** Sign up function */
|
|
20
|
+
signUp: (email: string, password: string, displayName?: string) => Promise<void>;
|
|
21
|
+
/** Sign in function */
|
|
22
|
+
signIn: (email: string, password: string) => Promise<void>;
|
|
23
|
+
/** Sign out function */
|
|
24
|
+
signOut: () => Promise<void>;
|
|
25
|
+
/** Continue as guest function */
|
|
26
|
+
continueAsGuest: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Hook for authentication state management
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const { user, isAuthenticated, signIn, signUp, signOut } = useAuth();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function useAuth(): UseAuthResult {
|
|
38
|
+
const [user, setUser] = useState<User | null>(null);
|
|
39
|
+
const [loading, setLoading] = useState(true);
|
|
40
|
+
const [isGuest, setIsGuest] = useState(false);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
try {
|
|
44
|
+
const service = getAuthService();
|
|
45
|
+
const unsubscribe = service.onAuthStateChange((currentUser) => {
|
|
46
|
+
setUser(currentUser);
|
|
47
|
+
setIsGuest(service.getIsGuestMode());
|
|
48
|
+
setLoading(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Set initial state
|
|
52
|
+
const currentUser = service.getCurrentUser();
|
|
53
|
+
setUser(currentUser);
|
|
54
|
+
setIsGuest(service.getIsGuestMode());
|
|
55
|
+
setLoading(false);
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
unsubscribe();
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Auth service not initialized
|
|
62
|
+
setUser(null);
|
|
63
|
+
setIsGuest(false);
|
|
64
|
+
setLoading(false);
|
|
65
|
+
return () => {};
|
|
66
|
+
}
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const signUp = useCallback(async (email: string, password: string, displayName?: string) => {
|
|
70
|
+
const service = getAuthService();
|
|
71
|
+
await service.signUp({ email, password, displayName });
|
|
72
|
+
// State will be updated via onAuthStateChange
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const signIn = useCallback(async (email: string, password: string) => {
|
|
76
|
+
const service = getAuthService();
|
|
77
|
+
await service.signIn({ email, password });
|
|
78
|
+
// State will be updated via onAuthStateChange
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const signOut = useCallback(async () => {
|
|
82
|
+
const service = getAuthService();
|
|
83
|
+
await service.signOut();
|
|
84
|
+
setUser(null);
|
|
85
|
+
setIsGuest(false);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const continueAsGuest = useCallback(async () => {
|
|
89
|
+
const service = getAuthService();
|
|
90
|
+
await service.setGuestMode();
|
|
91
|
+
setUser(null);
|
|
92
|
+
setIsGuest(true);
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
user,
|
|
97
|
+
loading,
|
|
98
|
+
isGuest,
|
|
99
|
+
isAuthenticated: !!user && !isGuest,
|
|
100
|
+
signUp,
|
|
101
|
+
signIn,
|
|
102
|
+
signOut,
|
|
103
|
+
continueAsGuest,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|