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.
Files changed (59) hide show
  1. package/README.md +574 -0
  2. package/dist/components/AuthScreen.d.ts +14 -0
  3. package/dist/components/AuthScreen.js +75 -0
  4. package/dist/components/LoginForm.d.ts +7 -0
  5. package/dist/components/LoginForm.js +180 -0
  6. package/dist/components/PasswordInput.d.ts +8 -0
  7. package/dist/components/PasswordInput.js +70 -0
  8. package/dist/components/SignUpForm.d.ts +8 -0
  9. package/dist/components/SignUpForm.js +198 -0
  10. package/dist/components/index.d.ts +3 -0
  11. package/dist/components/index.js +19 -0
  12. package/dist/core/AuthContext.d.ts +3 -0
  13. package/dist/core/AuthContext.js +10 -0
  14. package/dist/core/AuthProvider.d.ts +8 -0
  15. package/dist/core/AuthProvider.js +350 -0
  16. package/dist/core/index.d.ts +2 -0
  17. package/dist/core/index.js +18 -0
  18. package/dist/errors/errorMapper.d.ts +2 -0
  19. package/dist/errors/errorMapper.js +124 -0
  20. package/dist/errors/index.d.ts +1 -0
  21. package/dist/errors/index.js +17 -0
  22. package/dist/hooks/index.d.ts +1 -0
  23. package/dist/hooks/index.js +17 -0
  24. package/dist/hooks/useAuth.d.ts +2 -0
  25. package/dist/hooks/useAuth.js +13 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.js +27 -0
  28. package/dist/types/auth.types.d.ts +29 -0
  29. package/dist/types/auth.types.js +11 -0
  30. package/dist/types/config.types.d.ts +21 -0
  31. package/dist/types/config.types.js +12 -0
  32. package/dist/types/error.types.d.ts +23 -0
  33. package/dist/types/error.types.js +26 -0
  34. package/dist/types/index.d.ts +4 -0
  35. package/dist/types/index.js +21 -0
  36. package/dist/types/ui.types.d.ts +20 -0
  37. package/dist/types/ui.types.js +2 -0
  38. package/dist/utils/validation.d.ts +3 -0
  39. package/dist/utils/validation.js +29 -0
  40. package/package.json +62 -0
  41. package/src/components/AuthScreen.tsx +87 -0
  42. package/src/components/LoginForm.tsx +246 -0
  43. package/src/components/PasswordInput.tsx +56 -0
  44. package/src/components/SignUpForm.tsx +293 -0
  45. package/src/components/index.ts +3 -0
  46. package/src/core/AuthContext.tsx +6 -0
  47. package/src/core/AuthProvider.tsx +362 -0
  48. package/src/core/index.ts +2 -0
  49. package/src/errors/errorMapper.ts +139 -0
  50. package/src/errors/index.ts +1 -0
  51. package/src/hooks/index.ts +1 -0
  52. package/src/hooks/useAuth.ts +13 -0
  53. package/src/index.ts +12 -0
  54. package/src/types/auth.types.ts +43 -0
  55. package/src/types/config.types.ts +46 -0
  56. package/src/types/error.types.ts +31 -0
  57. package/src/types/index.ts +5 -0
  58. package/src/types/ui.types.ts +26 -0
  59. 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,3 @@
1
+ export * from './AuthScreen';
2
+ export * from './LoginForm';
3
+ export * from './SignUpForm';
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ export * from './AuthContext';
2
+ export * from './AuthProvider';