rn-swiftauth-sdk 1.0.1 → 1.0.3
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/README.md +229 -370
- package/dist/components/LoginForm.js +19 -11
- package/dist/components/SignUpForm.js +17 -16
- package/dist/core/AuthProvider.js +56 -110
- package/dist/errors/errorMapper.d.ts +10 -2
- package/dist/errors/errorMapper.js +61 -99
- package/dist/errors/exceptions.d.ts +82 -0
- package/dist/errors/exceptions.js +134 -0
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +19 -15
- package/dist/types/auth.types.d.ts +12 -4
- package/dist/types/auth.types.js +0 -1
- package/dist/types/error.types.d.ts +19 -9
- package/dist/types/error.types.js +35 -13
- package/package.json +1 -1
- package/src/components/LoginForm.tsx +21 -12
- package/src/components/SignUpForm.tsx +20 -31
- package/src/core/AuthProvider.tsx +84 -146
- package/src/errors/errorMapper.ts +97 -105
- package/src/errors/exceptions.ts +174 -0
- package/src/errors/index.ts +22 -1
- package/src/types/auth.types.ts +20 -17
- package/src/types/error.types.ts +42 -15
|
@@ -22,17 +22,19 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
|
|
|
22
22
|
signInWithEmail,
|
|
23
23
|
signInWithGoogle,
|
|
24
24
|
signInWithApple,
|
|
25
|
-
isLoading,
|
|
25
|
+
isLoading,
|
|
26
26
|
error,
|
|
27
|
-
config
|
|
27
|
+
config
|
|
28
28
|
} = useAuth();
|
|
29
29
|
|
|
30
30
|
const [email, setEmail] = useState('');
|
|
31
31
|
const [password, setPassword] = useState('');
|
|
32
32
|
|
|
33
|
-
// ✅ Validation Error State
|
|
34
33
|
const [validationErrors, setValidationErrors] = useState<{ email?: string; password?: string }>({});
|
|
35
34
|
|
|
35
|
+
//Check if form is filled to enable button
|
|
36
|
+
const isFormFilled = email.length > 0 && password.length > 0;
|
|
37
|
+
|
|
36
38
|
const handleLogin = async () => {
|
|
37
39
|
// 1. Reset previous errors
|
|
38
40
|
setValidationErrors({});
|
|
@@ -46,14 +48,17 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
|
|
|
46
48
|
email: emailErr || undefined,
|
|
47
49
|
password: passErr || undefined
|
|
48
50
|
});
|
|
49
|
-
return; //
|
|
51
|
+
return; //Stop if invalid
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// 3. Attempt Login
|
|
53
55
|
try {
|
|
54
|
-
|
|
56
|
+
//UPDATED: Clean Object Syntax
|
|
57
|
+
await signInWithEmail({ email, password });
|
|
55
58
|
} catch (e) {
|
|
56
59
|
// Auth errors handled by global state
|
|
60
|
+
// DX: Log it for the developer (Optional but helpful for debugging)
|
|
61
|
+
console.log('Login failed:', e);
|
|
57
62
|
}
|
|
58
63
|
};
|
|
59
64
|
|
|
@@ -87,7 +92,7 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
|
|
|
87
92
|
style={[
|
|
88
93
|
defaultStyles.input,
|
|
89
94
|
userStyles?.input,
|
|
90
|
-
validationErrors.email ? { borderColor: 'red' } : {}
|
|
95
|
+
validationErrors.email ? { borderColor: 'red' } : {}
|
|
91
96
|
]}
|
|
92
97
|
placeholder="Email"
|
|
93
98
|
value={email}
|
|
@@ -100,7 +105,6 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
|
|
|
100
105
|
placeholderTextColor="#999"
|
|
101
106
|
editable={!isLoading}
|
|
102
107
|
/>
|
|
103
|
-
{/* Validation Message */}
|
|
104
108
|
{validationErrors.email && (
|
|
105
109
|
<Text style={defaultStyles.validationText}>{validationErrors.email}</Text>
|
|
106
110
|
)}
|
|
@@ -124,11 +128,13 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
|
|
|
124
128
|
<TouchableOpacity
|
|
125
129
|
style={[
|
|
126
130
|
defaultStyles.button,
|
|
127
|
-
|
|
131
|
+
// Disable style if loading OR form is incomplete
|
|
132
|
+
(isLoading || !isFormFilled) && defaultStyles.buttonDisabled,
|
|
128
133
|
userStyles?.button
|
|
129
134
|
]}
|
|
130
135
|
onPress={handleLogin}
|
|
131
|
-
|
|
136
|
+
// Disable interaction if loading OR form is incomplete
|
|
137
|
+
disabled={isLoading || !isFormFilled}
|
|
132
138
|
>
|
|
133
139
|
{isLoading ? (
|
|
134
140
|
<ActivityIndicator color={userStyles?.loadingIndicatorColor || "#fff"} />
|
|
@@ -139,7 +145,7 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
|
|
|
139
145
|
)}
|
|
140
146
|
</TouchableOpacity>
|
|
141
147
|
|
|
142
|
-
{/* OAuth Section
|
|
148
|
+
{/* OAuth Section */}
|
|
143
149
|
{(config.enableGoogle || config.enableApple) && !isLoading && (
|
|
144
150
|
<>
|
|
145
151
|
<View style={defaultStyles.dividerContainer}>
|
|
@@ -190,7 +196,7 @@ const defaultStyles = StyleSheet.create({
|
|
|
190
196
|
backgroundColor: '#f5f5f5',
|
|
191
197
|
padding: 15,
|
|
192
198
|
borderRadius: 8,
|
|
193
|
-
marginBottom: 8,
|
|
199
|
+
marginBottom: 8,
|
|
194
200
|
borderWidth: 1,
|
|
195
201
|
borderColor: '#e0e0e0',
|
|
196
202
|
fontSize: 16,
|
|
@@ -203,7 +209,10 @@ const defaultStyles = StyleSheet.create({
|
|
|
203
209
|
alignItems: 'center',
|
|
204
210
|
marginTop: 8,
|
|
205
211
|
},
|
|
206
|
-
buttonDisabled: {
|
|
212
|
+
buttonDisabled: {
|
|
213
|
+
backgroundColor: '#a0cfff',
|
|
214
|
+
opacity: 0.7
|
|
215
|
+
},
|
|
207
216
|
buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 },
|
|
208
217
|
|
|
209
218
|
errorText: { color: 'red', marginBottom: 12, fontSize: 14, textAlign: 'center' },
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TouchableOpacity,
|
|
7
7
|
StyleSheet,
|
|
8
8
|
ActivityIndicator,
|
|
9
|
-
Platform
|
|
9
|
+
Platform,
|
|
10
10
|
} from 'react-native';
|
|
11
11
|
|
|
12
12
|
import { useAuth } from '../hooks/useAuth';
|
|
@@ -24,19 +24,20 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
24
24
|
signUpWithEmail,
|
|
25
25
|
signInWithGoogle,
|
|
26
26
|
signInWithApple,
|
|
27
|
-
isLoading,
|
|
27
|
+
isLoading,
|
|
28
28
|
error,
|
|
29
|
-
config
|
|
29
|
+
config
|
|
30
30
|
} = useAuth();
|
|
31
31
|
|
|
32
32
|
const [email, setEmail] = useState('');
|
|
33
33
|
const [password, setPassword] = useState('');
|
|
34
34
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
35
|
-
|
|
36
|
-
// ✅ Proper Validation State
|
|
37
35
|
const [validationErrors, setValidationErrors] = useState<{ email?: string; password?: string; confirm?: string }>({});
|
|
38
36
|
|
|
39
|
-
//
|
|
37
|
+
// 1. Smart Button Logic: Check if all fields have content
|
|
38
|
+
const isFormFilled = email.length > 0 && password.length > 0 && confirmPassword.length > 0;
|
|
39
|
+
|
|
40
|
+
// Password Requirements Logic
|
|
40
41
|
const requirements = [
|
|
41
42
|
{ label: "At least 6 characters", met: password.length >= 6 },
|
|
42
43
|
{ label: "Contains a number", met: /\d/.test(password) },
|
|
@@ -44,19 +45,16 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
44
45
|
];
|
|
45
46
|
|
|
46
47
|
const handleSignUp = async () => {
|
|
47
|
-
// 1. Reset Errors
|
|
48
48
|
setValidationErrors({});
|
|
49
49
|
|
|
50
50
|
// 2. Validate Inputs
|
|
51
51
|
const emailErr = validateEmail(email);
|
|
52
52
|
const passErr = validatePasswordSignup(password);
|
|
53
|
-
|
|
54
53
|
let confirmErr;
|
|
55
54
|
if (password !== confirmPassword) {
|
|
56
55
|
confirmErr = "Passwords do not match.";
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
// 3. Check if any errors exist
|
|
60
58
|
if (emailErr || passErr || confirmErr) {
|
|
61
59
|
setValidationErrors({
|
|
62
60
|
email: emailErr || undefined,
|
|
@@ -66,11 +64,11 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
66
64
|
return;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
// 4. Attempt Sign Up
|
|
70
67
|
try {
|
|
71
|
-
|
|
68
|
+
// ✅ UPDATED: New Object Syntax
|
|
69
|
+
await signUpWithEmail({ email, password });
|
|
72
70
|
} catch (e) {
|
|
73
|
-
|
|
71
|
+
console.error('Sign Up Failed:', e);
|
|
74
72
|
}
|
|
75
73
|
};
|
|
76
74
|
|
|
@@ -92,7 +90,6 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
92
90
|
|
|
93
91
|
return (
|
|
94
92
|
<View style={[defaultStyles.container, userStyles?.container]}>
|
|
95
|
-
{/* Global API Error */}
|
|
96
93
|
{error && (
|
|
97
94
|
<Text style={[defaultStyles.globalError, userStyles?.errorText]}>
|
|
98
95
|
{error.message}
|
|
@@ -177,11 +174,13 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
177
174
|
<TouchableOpacity
|
|
178
175
|
style={[
|
|
179
176
|
defaultStyles.button,
|
|
180
|
-
|
|
177
|
+
// Disable style if loading OR form incomplete
|
|
178
|
+
(isLoading || !isFormFilled) && defaultStyles.buttonDisabled,
|
|
181
179
|
userStyles?.button
|
|
182
180
|
]}
|
|
183
181
|
onPress={handleSignUp}
|
|
184
|
-
|
|
182
|
+
// Disable interaction if loading OR form incomplete
|
|
183
|
+
disabled={isLoading || !isFormFilled}
|
|
185
184
|
>
|
|
186
185
|
{isLoading ? (
|
|
187
186
|
<ActivityIndicator color={userStyles?.loadingIndicatorColor || "#fff"} />
|
|
@@ -192,7 +191,7 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
192
191
|
)}
|
|
193
192
|
</TouchableOpacity>
|
|
194
193
|
|
|
195
|
-
{/* OAuth Section
|
|
194
|
+
{/* OAuth Section */}
|
|
196
195
|
{(config.enableGoogle || config.enableApple) && !isLoading && (
|
|
197
196
|
<>
|
|
198
197
|
<View style={defaultStyles.dividerContainer}>
|
|
@@ -201,7 +200,6 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
201
200
|
<View style={defaultStyles.divider} />
|
|
202
201
|
</View>
|
|
203
202
|
|
|
204
|
-
{/* Google */}
|
|
205
203
|
{config.enableGoogle && (
|
|
206
204
|
<TouchableOpacity
|
|
207
205
|
style={[defaultStyles.oauthButton, defaultStyles.googleButton]}
|
|
@@ -213,7 +211,6 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
213
211
|
</TouchableOpacity>
|
|
214
212
|
)}
|
|
215
213
|
|
|
216
|
-
{/* Apple */}
|
|
217
214
|
{config.enableApple && Platform.OS === 'ios' && (
|
|
218
215
|
<TouchableOpacity
|
|
219
216
|
style={[defaultStyles.oauthButton, defaultStyles.appleButton]}
|
|
@@ -232,17 +229,15 @@ export const SignUpForm = ({ styles: userStyles, showHints = true }: SignUpFormP
|
|
|
232
229
|
|
|
233
230
|
const defaultStyles = StyleSheet.create({
|
|
234
231
|
container: { width: '100%', marginVertical: 10 },
|
|
235
|
-
|
|
236
232
|
input: {
|
|
237
233
|
backgroundColor: '#f5f5f5',
|
|
238
234
|
padding: 15,
|
|
239
235
|
borderRadius: 8,
|
|
240
|
-
marginBottom: 8,
|
|
236
|
+
marginBottom: 8,
|
|
241
237
|
borderWidth: 1,
|
|
242
238
|
borderColor: '#e0e0e0',
|
|
243
239
|
fontSize: 16,
|
|
244
240
|
},
|
|
245
|
-
|
|
246
241
|
button: {
|
|
247
242
|
backgroundColor: '#34C759',
|
|
248
243
|
padding: 15,
|
|
@@ -250,15 +245,13 @@ const defaultStyles = StyleSheet.create({
|
|
|
250
245
|
alignItems: 'center',
|
|
251
246
|
marginTop: 8,
|
|
252
247
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
248
|
+
buttonDisabled: {
|
|
249
|
+
backgroundColor: '#9ce4ae',
|
|
250
|
+
opacity: 0.7
|
|
251
|
+
},
|
|
256
252
|
buttonText: { color: '#fff', fontWeight: '600', fontSize: 16 },
|
|
257
|
-
|
|
258
253
|
globalError: { color: 'red', marginBottom: 12, fontSize: 14, textAlign: 'center' },
|
|
259
254
|
validationText: { color: 'red', fontSize: 12, marginBottom: 10, marginLeft: 4, marginTop: -4 },
|
|
260
|
-
|
|
261
|
-
// OAuth Styles
|
|
262
255
|
dividerContainer: {
|
|
263
256
|
flexDirection: 'row',
|
|
264
257
|
alignItems: 'center',
|
|
@@ -266,7 +259,6 @@ const defaultStyles = StyleSheet.create({
|
|
|
266
259
|
},
|
|
267
260
|
divider: { flex: 1, height: 1, backgroundColor: '#e0e0e0' },
|
|
268
261
|
dividerText: { marginHorizontal: 16, color: '#666', fontSize: 14 },
|
|
269
|
-
|
|
270
262
|
oauthButton: {
|
|
271
263
|
padding: 15,
|
|
272
264
|
borderRadius: 8,
|
|
@@ -281,11 +273,8 @@ const defaultStyles = StyleSheet.create({
|
|
|
281
273
|
borderColor: '#e0e0e0',
|
|
282
274
|
},
|
|
283
275
|
googleButtonText: { color: '#000', fontSize: 16, fontWeight: '500' },
|
|
284
|
-
|
|
285
276
|
appleButton: { backgroundColor: '#000' },
|
|
286
277
|
appleButtonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
|
|
287
|
-
|
|
288
|
-
// Password Hint Styles
|
|
289
278
|
hintContainer: { marginBottom: 15, paddingLeft: 5 },
|
|
290
279
|
hintRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 4 },
|
|
291
280
|
hintText: { color: '#666', fontSize: 12 },
|
|
@@ -1,39 +1,30 @@
|
|
|
1
1
|
import React, { useEffect, useState, ReactNode, useMemo } from 'react';
|
|
2
2
|
import { Platform } from 'react-native';
|
|
3
3
|
import { initializeApp, getApps, getApp, FirebaseApp } from 'firebase/app';
|
|
4
|
-
|
|
5
|
-
// Firebase Auth
|
|
6
|
-
import {
|
|
7
|
-
getAuth,
|
|
8
|
-
initializeAuth,
|
|
9
|
-
onAuthStateChanged,
|
|
10
|
-
inMemoryPersistence,
|
|
11
|
-
User as FirebaseUser,
|
|
12
|
-
Auth as FirebaseAuth,
|
|
13
|
-
createUserWithEmailAndPassword,
|
|
14
|
-
signInWithEmailAndPassword,
|
|
15
|
-
GoogleAuthProvider,
|
|
16
|
-
OAuthProvider,
|
|
17
|
-
signInWithCredential,
|
|
18
|
-
} from 'firebase/auth';
|
|
19
|
-
|
|
20
|
-
// The Hack: Import for getReactNativePersistence
|
|
21
|
-
import * as firebaseAuth from 'firebase/auth';
|
|
22
|
-
// @ts-ignore
|
|
23
|
-
const getReactNativePersistence = (firebaseAuth as any).getReactNativePersistence;
|
|
24
|
-
|
|
4
|
+
import * as FirebaseAuth from 'firebase/auth';
|
|
25
5
|
import ReactNativeAsyncStorage from '@react-native-async-storage/async-storage';
|
|
26
|
-
|
|
27
|
-
// PROPER GOOGLE SIGN-IN
|
|
28
6
|
import { GoogleSignin } from '@react-native-google-signin/google-signin';
|
|
29
|
-
|
|
30
|
-
// Apple Sign-In (Expo)
|
|
31
7
|
import * as AppleAuthentication from 'expo-apple-authentication';
|
|
32
8
|
import * as Crypto from 'expo-crypto';
|
|
33
|
-
|
|
34
|
-
|
|
9
|
+
import {
|
|
10
|
+
AuthException,
|
|
11
|
+
mapFirebaseError,
|
|
12
|
+
ConfigurationException,
|
|
13
|
+
AppleSignInNotSupportedException,
|
|
14
|
+
AppleSignInCancelledException,
|
|
15
|
+
GoogleSignInCancelledException,
|
|
16
|
+
} from '../errors';
|
|
35
17
|
import { AuthContext } from './AuthContext';
|
|
36
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
AuthConfig,
|
|
20
|
+
AuthStatus,
|
|
21
|
+
User,
|
|
22
|
+
ProviderErrorCodes,
|
|
23
|
+
EmailSignInOptions,
|
|
24
|
+
EmailSignUpOptions
|
|
25
|
+
} from '../types';
|
|
26
|
+
|
|
27
|
+
const getReactNativePersistence = (FirebaseAuth as any).getReactNativePersistence;
|
|
37
28
|
|
|
38
29
|
interface AuthProviderProps {
|
|
39
30
|
config: AuthConfig;
|
|
@@ -43,18 +34,16 @@ interface AuthProviderProps {
|
|
|
43
34
|
export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children }) => {
|
|
44
35
|
const [user, setUser] = useState<User | null>(null);
|
|
45
36
|
const [status, setStatus] = useState<AuthStatus>(AuthStatus.LOADING);
|
|
46
|
-
const [error, setError] = useState<
|
|
47
|
-
const [firebaseAuthInstance, setFirebaseAuthInstance] = useState<FirebaseAuth | null>(null);
|
|
48
|
-
|
|
49
|
-
// ✅ NEW: Explicit loading state for initial app load vs action loading
|
|
37
|
+
const [error, setError] = useState<AuthException | null>(null);
|
|
38
|
+
const [firebaseAuthInstance, setFirebaseAuthInstance] = useState<FirebaseAuth.Auth | null>(null);
|
|
50
39
|
const [isDataLoading, setIsDataLoading] = useState(true);
|
|
51
40
|
|
|
41
|
+
// --- INITIALIZATION ---
|
|
52
42
|
useEffect(() => {
|
|
53
43
|
let app: FirebaseApp;
|
|
54
|
-
let auth: FirebaseAuth;
|
|
44
|
+
let auth: FirebaseAuth.Auth;
|
|
55
45
|
|
|
56
46
|
if (!getApps().length) {
|
|
57
|
-
// 1. Initialize App
|
|
58
47
|
app = initializeApp({
|
|
59
48
|
apiKey: config.apiKey,
|
|
60
49
|
authDomain: config.authDomain,
|
|
@@ -64,42 +53,37 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
64
53
|
appId: config.appId,
|
|
65
54
|
});
|
|
66
55
|
|
|
67
|
-
// 2. Select Persistence Strategy
|
|
68
56
|
const selectedPersistence = config.persistence === 'memory'
|
|
69
|
-
? inMemoryPersistence
|
|
57
|
+
? FirebaseAuth.inMemoryPersistence
|
|
70
58
|
: getReactNativePersistence(ReactNativeAsyncStorage);
|
|
71
59
|
|
|
72
|
-
|
|
73
|
-
auth = initializeAuth(app, {
|
|
60
|
+
auth = FirebaseAuth.initializeAuth(app, {
|
|
74
61
|
persistence: selectedPersistence
|
|
75
62
|
});
|
|
76
|
-
|
|
77
63
|
} else {
|
|
78
64
|
app = getApp();
|
|
79
|
-
auth = getAuth(app);
|
|
65
|
+
auth = FirebaseAuth.getAuth(app);
|
|
80
66
|
}
|
|
81
67
|
|
|
82
68
|
setFirebaseAuthInstance(auth);
|
|
83
69
|
|
|
84
|
-
// 4. Configure Google Sign-In if enabled
|
|
85
70
|
if (config.enableGoogle && config.googleWebClientId) {
|
|
86
71
|
try {
|
|
87
72
|
GoogleSignin.configure({
|
|
88
73
|
webClientId: config.googleWebClientId,
|
|
89
74
|
offlineAccess: true,
|
|
90
|
-
iosClientId: config.googleIOSClientId,
|
|
75
|
+
iosClientId: config.googleIOSClientId,
|
|
91
76
|
});
|
|
92
|
-
console.log('
|
|
77
|
+
console.log('Google Sign-In configured successfully');
|
|
93
78
|
} catch (err) {
|
|
94
|
-
console.error('
|
|
79
|
+
console.error('Google Sign-In configuration failed:', err);
|
|
95
80
|
}
|
|
96
81
|
}
|
|
97
82
|
|
|
98
|
-
const unsubscribe = onAuthStateChanged(auth, async (fbUser
|
|
83
|
+
const unsubscribe = FirebaseAuth.onAuthStateChanged(auth, async (fbUser) => {
|
|
99
84
|
try {
|
|
100
85
|
if (fbUser) {
|
|
101
86
|
try {
|
|
102
|
-
// Force token refresh to ensure validity on load
|
|
103
87
|
const token = await fbUser.getIdToken(true);
|
|
104
88
|
setUser({
|
|
105
89
|
uid: fbUser.uid,
|
|
@@ -112,10 +96,13 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
112
96
|
setStatus(AuthStatus.AUTHENTICATED);
|
|
113
97
|
} catch (tokenError: any) {
|
|
114
98
|
console.error('Token retrieval error:', tokenError);
|
|
115
|
-
if (tokenError.code ===
|
|
99
|
+
if (tokenError.code === ProviderErrorCodes.USER_TOKEN_EXPIRED ||
|
|
100
|
+
tokenError.code === ProviderErrorCodes.NULL_USER) {
|
|
116
101
|
setStatus(AuthStatus.TOKEN_EXPIRED);
|
|
102
|
+
setError(mapFirebaseError(tokenError));
|
|
117
103
|
} else {
|
|
118
104
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
105
|
+
setError(mapFirebaseError(tokenError));
|
|
119
106
|
}
|
|
120
107
|
setUser(null);
|
|
121
108
|
}
|
|
@@ -126,60 +113,51 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
126
113
|
} catch (err) {
|
|
127
114
|
console.error("Auth State Error:", err);
|
|
128
115
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
116
|
+
setError(mapFirebaseError(err));
|
|
129
117
|
} finally {
|
|
130
|
-
// ✅ Stop initial loading spinner once Firebase has checked storage
|
|
131
118
|
setIsDataLoading(false);
|
|
132
119
|
}
|
|
133
|
-
}, (err) => {
|
|
134
|
-
console.error("Auth State Error:", err);
|
|
135
|
-
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
136
|
-
setIsDataLoading(false);
|
|
137
120
|
});
|
|
138
121
|
|
|
139
122
|
return () => unsubscribe();
|
|
140
123
|
}, [config]);
|
|
141
124
|
|
|
142
|
-
//
|
|
143
|
-
|
|
125
|
+
// --- ACTIONS ---
|
|
126
|
+
|
|
127
|
+
const signInWithEmail = async ({ email, password }: EmailSignInOptions) => {
|
|
144
128
|
if (!firebaseAuthInstance) return;
|
|
145
129
|
try {
|
|
146
130
|
setError(null);
|
|
147
131
|
setStatus(AuthStatus.LOADING);
|
|
148
|
-
await signInWithEmailAndPassword(firebaseAuthInstance, email,
|
|
149
|
-
} catch (err) {
|
|
150
|
-
const
|
|
151
|
-
setError(
|
|
132
|
+
await FirebaseAuth.signInWithEmailAndPassword(firebaseAuthInstance, email, password);
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
const mappedException = mapFirebaseError(err);
|
|
135
|
+
setError(mappedException);
|
|
152
136
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
153
|
-
throw
|
|
137
|
+
throw mappedException;
|
|
154
138
|
}
|
|
155
139
|
};
|
|
156
140
|
|
|
157
|
-
|
|
158
|
-
const signUpWithEmail = async (email: string, pass: string) => {
|
|
141
|
+
const signUpWithEmail = async ({ email, password }: EmailSignUpOptions) => {
|
|
159
142
|
if (!firebaseAuthInstance) return;
|
|
160
143
|
try {
|
|
161
144
|
setError(null);
|
|
162
145
|
setStatus(AuthStatus.LOADING);
|
|
163
|
-
await createUserWithEmailAndPassword(firebaseAuthInstance, email,
|
|
164
|
-
} catch (err) {
|
|
165
|
-
const
|
|
166
|
-
setError(
|
|
146
|
+
await FirebaseAuth.createUserWithEmailAndPassword(firebaseAuthInstance, email, password);
|
|
147
|
+
} catch (err: any) {
|
|
148
|
+
const mappedException = mapFirebaseError(err);
|
|
149
|
+
setError(mappedException);
|
|
167
150
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
168
|
-
throw
|
|
151
|
+
throw mappedException;
|
|
169
152
|
}
|
|
170
153
|
};
|
|
171
154
|
|
|
172
|
-
// PROPER GOOGLE SIGN-IN using @react-native-google-signin/google-signin
|
|
173
155
|
const signInWithGoogle = async () => {
|
|
174
|
-
if (!firebaseAuthInstance)
|
|
175
|
-
throw new Error('Firebase not initialized');
|
|
176
|
-
}
|
|
177
|
-
|
|
156
|
+
if (!firebaseAuthInstance) throw new Error('Firebase not initialized');
|
|
178
157
|
if (!config.enableGoogle || !config.googleWebClientId) {
|
|
179
|
-
const configError
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
};
|
|
158
|
+
const configError = new ConfigurationException(
|
|
159
|
+
'Google Auth not configured. Missing googleWebClientId.'
|
|
160
|
+
);
|
|
183
161
|
setError(configError);
|
|
184
162
|
throw configError;
|
|
185
163
|
}
|
|
@@ -192,71 +170,41 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
192
170
|
const userInfo = await GoogleSignin.signIn();
|
|
193
171
|
const idToken = userInfo.data?.idToken;
|
|
194
172
|
|
|
195
|
-
if (!idToken)
|
|
196
|
-
throw new Error('No ID token received from Google Sign-In');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const credential = GoogleAuthProvider.credential(idToken);
|
|
200
|
-
await signInWithCredential(firebaseAuthInstance, credential);
|
|
173
|
+
if (!idToken) throw new Error('No ID token received from Google Sign-In');
|
|
201
174
|
|
|
202
|
-
|
|
175
|
+
const credential = FirebaseAuth.GoogleAuthProvider.credential(idToken);
|
|
176
|
+
await FirebaseAuth.signInWithCredential(firebaseAuthInstance, credential);
|
|
177
|
+
console.log('Google Sign-In successful');
|
|
203
178
|
|
|
204
179
|
} catch (err: any) {
|
|
205
|
-
console.error('
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
code: AuthErrorCode.GOOGLE_SIGN_IN_CANCELLED,
|
|
211
|
-
message: 'Google Sign-In was cancelled',
|
|
212
|
-
originalError: err
|
|
213
|
-
};
|
|
214
|
-
// Reset status if cancelled, don't leave it loading
|
|
180
|
+
console.error('Google Sign-In Error:', err);
|
|
181
|
+
|
|
182
|
+
if (err.code === ProviderErrorCodes.GOOGLE_CANCELLED) {
|
|
183
|
+
const cancelError = new GoogleSignInCancelledException(err);
|
|
184
|
+
setError(cancelError);
|
|
215
185
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
216
186
|
return;
|
|
217
|
-
} else if (err.code === 'IN_PROGRESS') {
|
|
218
|
-
mappedError = {
|
|
219
|
-
code: AuthErrorCode.GOOGLE_SIGN_IN_IN_PROGRESS,
|
|
220
|
-
message: 'Google Sign-In is already in progress',
|
|
221
|
-
originalError: err
|
|
222
|
-
};
|
|
223
|
-
} else if (err.code === 'PLAY_SERVICES_NOT_AVAILABLE') {
|
|
224
|
-
mappedError = {
|
|
225
|
-
code: AuthErrorCode.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE,
|
|
226
|
-
message: 'Google Play Services are not available. Please update Google Play Services.',
|
|
227
|
-
originalError: err
|
|
228
|
-
};
|
|
229
|
-
} else {
|
|
230
|
-
mappedError = mapFirebaseError(err);
|
|
231
187
|
}
|
|
232
188
|
|
|
233
|
-
|
|
189
|
+
const mappedException = mapFirebaseError(err);
|
|
190
|
+
setError(mappedException);
|
|
234
191
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
235
|
-
throw
|
|
192
|
+
throw mappedException;
|
|
236
193
|
}
|
|
237
194
|
};
|
|
238
195
|
|
|
239
|
-
// Apple Sign-In using expo-apple-authentication
|
|
240
196
|
const signInWithApple = async () => {
|
|
241
|
-
if (!firebaseAuthInstance)
|
|
242
|
-
throw new Error('Firebase not initialized');
|
|
243
|
-
}
|
|
197
|
+
if (!firebaseAuthInstance) throw new Error('Firebase not initialized');
|
|
244
198
|
|
|
245
199
|
if (Platform.OS !== 'ios') {
|
|
246
|
-
const platformError
|
|
247
|
-
code: AuthErrorCode.APPLE_SIGN_IN_NOT_SUPPORTED,
|
|
248
|
-
message: 'Apple Sign-In is only available on iOS devices',
|
|
249
|
-
};
|
|
200
|
+
const platformError = new AppleSignInNotSupportedException();
|
|
250
201
|
setError(platformError);
|
|
251
202
|
throw platformError;
|
|
252
203
|
}
|
|
253
204
|
|
|
254
205
|
const isAvailable = await AppleAuthentication.isAvailableAsync();
|
|
255
206
|
if (!isAvailable) {
|
|
256
|
-
const availabilityError
|
|
257
|
-
code: AuthErrorCode.APPLE_SIGN_IN_NOT_SUPPORTED,
|
|
258
|
-
message: 'Apple Sign-In is not available on this device (requires iOS 13+)',
|
|
259
|
-
};
|
|
207
|
+
const availabilityError = new AppleSignInNotSupportedException();
|
|
260
208
|
setError(availabilityError);
|
|
261
209
|
throw availabilityError;
|
|
262
210
|
}
|
|
@@ -266,10 +214,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
266
214
|
setStatus(AuthStatus.LOADING);
|
|
267
215
|
|
|
268
216
|
const nonce = Math.random().toString(36).substring(2, 10);
|
|
269
|
-
const hashedNonce = await Crypto.digestStringAsync(
|
|
270
|
-
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
271
|
-
nonce
|
|
272
|
-
);
|
|
217
|
+
const hashedNonce = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, nonce);
|
|
273
218
|
|
|
274
219
|
const appleCredential = await AppleAuthentication.signInAsync({
|
|
275
220
|
requestedScopes: [
|
|
@@ -281,41 +226,34 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
281
226
|
|
|
282
227
|
const { identityToken } = appleCredential;
|
|
283
228
|
|
|
284
|
-
if (!identityToken)
|
|
285
|
-
throw new Error('No identity token received from Apple');
|
|
286
|
-
}
|
|
229
|
+
if (!identityToken) throw new Error('No identity token received from Apple');
|
|
287
230
|
|
|
288
|
-
const provider = new OAuthProvider('apple.com');
|
|
231
|
+
const provider = new FirebaseAuth.OAuthProvider('apple.com');
|
|
289
232
|
const credential = provider.credential({
|
|
290
233
|
idToken: identityToken,
|
|
291
234
|
rawNonce: nonce,
|
|
292
235
|
});
|
|
293
236
|
|
|
294
|
-
await signInWithCredential(firebaseAuthInstance, credential);
|
|
295
|
-
console.log('
|
|
237
|
+
await FirebaseAuth.signInWithCredential(firebaseAuthInstance, credential);
|
|
238
|
+
console.log('Apple Sign-In successful');
|
|
296
239
|
|
|
297
240
|
} catch (err: any) {
|
|
298
|
-
console.error('
|
|
299
|
-
|
|
300
|
-
if (err.code ===
|
|
301
|
-
const cancelError
|
|
302
|
-
code: AuthErrorCode.APPLE_SIGN_IN_CANCELLED,
|
|
303
|
-
message: 'Apple Sign-In was cancelled',
|
|
304
|
-
originalError: err
|
|
305
|
-
};
|
|
241
|
+
console.error('Apple Sign-In Error:', err);
|
|
242
|
+
|
|
243
|
+
if (err.code === ProviderErrorCodes.APPLE_CANCELLED) {
|
|
244
|
+
const cancelError = new AppleSignInCancelledException(err);
|
|
306
245
|
setError(cancelError);
|
|
307
246
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
308
247
|
return;
|
|
309
248
|
}
|
|
310
249
|
|
|
311
|
-
const
|
|
312
|
-
setError(
|
|
250
|
+
const mappedException = mapFirebaseError(err);
|
|
251
|
+
setError(mappedException);
|
|
313
252
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
314
|
-
throw
|
|
253
|
+
throw mappedException;
|
|
315
254
|
}
|
|
316
255
|
};
|
|
317
256
|
|
|
318
|
-
// Sign Out
|
|
319
257
|
const signOut = async () => {
|
|
320
258
|
try {
|
|
321
259
|
if (firebaseAuthInstance) {
|
|
@@ -328,9 +266,11 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
328
266
|
console.log('Google sign-out skipped or failed:', googleSignOutError);
|
|
329
267
|
}
|
|
330
268
|
}
|
|
331
|
-
console.log('
|
|
269
|
+
console.log('Sign out successful');
|
|
332
270
|
} catch (err) {
|
|
333
|
-
console.error('
|
|
271
|
+
console.error('Sign out error:', err);
|
|
272
|
+
const signOutError = mapFirebaseError(err);
|
|
273
|
+
setError(signOutError);
|
|
334
274
|
setUser(null);
|
|
335
275
|
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
336
276
|
}
|
|
@@ -341,10 +281,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
341
281
|
const value = useMemo(() => ({
|
|
342
282
|
user,
|
|
343
283
|
status,
|
|
344
|
-
// ✅ NEW: Combine internal loading with AuthStatus
|
|
345
284
|
isLoading: isDataLoading || status === AuthStatus.LOADING,
|
|
346
285
|
error,
|
|
347
|
-
// ✅ NEW: Expose config for UI to read
|
|
348
286
|
config,
|
|
349
287
|
signInWithEmail,
|
|
350
288
|
signUpWithEmail,
|