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