rn-swiftauth-sdk 1.0.2 → 1.0.4

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.
@@ -6,7 +6,8 @@ import {
6
6
  TouchableOpacity,
7
7
  StyleSheet,
8
8
  ActivityIndicator,
9
- Platform
9
+ Platform,
10
+ Alert
10
11
  } from 'react-native';
11
12
  import { useAuth } from '../hooks/useAuth';
12
13
  import { AuthScreenStyles } from '../types';
@@ -22,24 +23,49 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
22
23
  signInWithEmail,
23
24
  signInWithGoogle,
24
25
  signInWithApple,
26
+ sendPasswordReset,
25
27
  isLoading,
26
28
  error,
27
- config
29
+ config,
30
+ clearError
28
31
  } = useAuth();
29
32
 
30
33
  const [email, setEmail] = useState('');
31
34
  const [password, setPassword] = useState('');
35
+ const [isResetMode, setIsResetMode] = useState(false); // ✅ 2. Mode Toggle State
32
36
 
33
37
  const [validationErrors, setValidationErrors] = useState<{ email?: string; password?: string }>({});
34
38
 
35
- //Check if form is filled to enable button
36
- const isFormFilled = email.length > 0 && password.length > 0;
39
+ // 3. Dynamic check: If resetting, we only need Email. If logging in, we need both.
40
+ const isFormFilled = isResetMode
41
+ ? email.length > 0
42
+ : (email.length > 0 && password.length > 0);
37
43
 
38
- const handleLogin = async () => {
39
- // 1. Reset previous errors
44
+ // 4. New Handler for Password Reset
45
+ const handleResetPassword = async () => {
40
46
  setValidationErrors({});
47
+ const emailErr = validateEmail(email);
48
+ if (emailErr) {
49
+ setValidationErrors({ email: emailErr });
50
+ return;
51
+ }
52
+
53
+ try {
54
+ await sendPasswordReset(email);
55
+ Alert.alert(
56
+ "Check your email",
57
+ "If an account exists with this email, a password reset link has been sent."
58
+ );
59
+ setIsResetMode(false); // Go back to login screen
60
+ } catch (e) {
61
+ console.log('Reset failed:', e);
62
+ }
63
+ };
41
64
 
42
- // 2. Validate Inputs
65
+ const handleLogin = async () => {
66
+ setValidationErrors({});
67
+
68
+ // Validate Inputs
43
69
  const emailErr = validateEmail(email);
44
70
  const passErr = validatePasswordLogin(password);
45
71
 
@@ -48,16 +74,12 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
48
74
  email: emailErr || undefined,
49
75
  password: passErr || undefined
50
76
  });
51
- return; //Stop if invalid
77
+ return;
52
78
  }
53
79
 
54
- // 3. Attempt Login
55
80
  try {
56
- //UPDATED: Clean Object Syntax
57
81
  await signInWithEmail({ email, password });
58
82
  } catch (e) {
59
- // Auth errors handled by global state
60
- // DX: Log it for the developer (Optional but helpful for debugging)
61
83
  console.log('Login failed:', e);
62
84
  }
63
85
  };
@@ -78,16 +100,30 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
78
100
  }
79
101
  };
80
102
 
103
+
104
+ const toggleResetMode = () => {
105
+ setIsResetMode(!isResetMode);
106
+ clearError();
107
+ setValidationErrors({});
108
+
109
+ setPassword('');
110
+ };
111
+
81
112
  return (
82
113
  <View style={[defaultStyles.container, userStyles?.container]}>
83
- {/* Global API Error */}
114
+
84
115
  {error && (
85
116
  <Text style={[defaultStyles.errorText, userStyles?.errorText]}>
86
117
  {error.message}
87
118
  </Text>
88
119
  )}
89
120
 
90
- {/* Email Input */}
121
+
122
+ {isResetMode && (
123
+ <Text style={defaultStyles.modeTitle}>Reset Password</Text>
124
+ )}
125
+
126
+
91
127
  <TextInput
92
128
  style={[
93
129
  defaultStyles.input,
@@ -109,44 +145,69 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
109
145
  <Text style={defaultStyles.validationText}>{validationErrors.email}</Text>
110
146
  )}
111
147
 
112
- {/* Password Input */}
113
- <PasswordInput
114
- styles={userStyles}
115
- placeholder="Password"
116
- value={password}
117
- onChangeText={(text) => {
118
- setPassword(text);
119
- if (validationErrors.password) setValidationErrors({...validationErrors, password: undefined});
120
- }}
121
- editable={!isLoading}
122
- />
123
- {validationErrors.password && (
124
- <Text style={defaultStyles.validationText}>{validationErrors.password}</Text>
148
+
149
+ {!isResetMode && (
150
+ <>
151
+ <PasswordInput
152
+ styles={userStyles}
153
+ placeholder="Password"
154
+ value={password}
155
+ onChangeText={(text) => {
156
+ setPassword(text);
157
+ if (validationErrors.password) setValidationErrors({...validationErrors, password: undefined});
158
+ }}
159
+ editable={!isLoading}
160
+ />
161
+ {validationErrors.password && (
162
+ <Text style={defaultStyles.validationText}>{validationErrors.password}</Text>
163
+ )}
164
+
165
+
166
+ <TouchableOpacity
167
+ style={defaultStyles.forgotPasswordContainer}
168
+ onPress={toggleResetMode}
169
+ disabled={isLoading}
170
+ >
171
+ <Text style={[defaultStyles.forgotPasswordText, userStyles?.linkText]}>
172
+ Forgot Password?
173
+ </Text>
174
+ </TouchableOpacity>
175
+ </>
125
176
  )}
126
177
 
127
- {/* Sign In Button */}
178
+
128
179
  <TouchableOpacity
129
180
  style={[
130
181
  defaultStyles.button,
131
- // Disable style if loading OR form is incomplete
132
182
  (isLoading || !isFormFilled) && defaultStyles.buttonDisabled,
133
183
  userStyles?.button
134
184
  ]}
135
- onPress={handleLogin}
136
- // Disable interaction if loading OR form is incomplete
185
+
186
+ onPress={isResetMode ? handleResetPassword : handleLogin}
137
187
  disabled={isLoading || !isFormFilled}
138
188
  >
139
189
  {isLoading ? (
140
190
  <ActivityIndicator color={userStyles?.loadingIndicatorColor || "#fff"} />
141
191
  ) : (
142
192
  <Text style={[defaultStyles.buttonText, userStyles?.buttonText]}>
143
- Sign In
193
+ {isResetMode ? "Send Reset Link" : "Sign In"}
144
194
  </Text>
145
195
  )}
146
196
  </TouchableOpacity>
147
197
 
148
- {/* OAuth Section */}
149
- {(config.enableGoogle || config.enableApple) && !isLoading && (
198
+
199
+ {isResetMode && (
200
+ <TouchableOpacity
201
+ style={defaultStyles.cancelButton}
202
+ onPress={toggleResetMode}
203
+ disabled={isLoading}
204
+ >
205
+ <Text style={defaultStyles.cancelButtonText}>Back to Sign In</Text>
206
+ </TouchableOpacity>
207
+ )}
208
+
209
+
210
+ {!isResetMode && (config.enableGoogle || config.enableApple) && !isLoading && (
150
211
  <>
151
212
  <View style={defaultStyles.dividerContainer}>
152
213
  <View style={defaultStyles.divider} />
@@ -157,30 +218,20 @@ export const LoginForm = ({ styles: userStyles }: LoginFormProps) => {
157
218
  {/* Google Button */}
158
219
  {config.enableGoogle && (
159
220
  <TouchableOpacity
160
- style={[
161
- defaultStyles.oauthButton,
162
- defaultStyles.googleButton,
163
- ]}
221
+ style={[defaultStyles.oauthButton, defaultStyles.googleButton]}
164
222
  onPress={handleGoogleSignIn}
165
223
  >
166
- <Text style={defaultStyles.googleButtonText}>
167
- Continue with Google
168
- </Text>
224
+ <Text style={defaultStyles.googleButtonText}>Continue with Google</Text>
169
225
  </TouchableOpacity>
170
226
  )}
171
227
 
172
228
  {/* Apple Button (iOS Only) */}
173
229
  {config.enableApple && Platform.OS === 'ios' && (
174
230
  <TouchableOpacity
175
- style={[
176
- defaultStyles.oauthButton,
177
- defaultStyles.appleButton,
178
- ]}
231
+ style={[defaultStyles.oauthButton, defaultStyles.appleButton]}
179
232
  onPress={handleAppleSignIn}
180
233
  >
181
- <Text style={defaultStyles.appleButtonText}>
182
- Continue with Apple
183
- </Text>
234
+ <Text style={defaultStyles.appleButtonText}>Continue with Apple</Text>
184
235
  </TouchableOpacity>
185
236
  )}
186
237
  </>
@@ -252,4 +303,33 @@ const defaultStyles = StyleSheet.create({
252
303
  fontSize: 16,
253
304
  fontWeight: '600',
254
305
  },
306
+
307
+
308
+ forgotPasswordContainer: {
309
+ alignSelf: 'flex-end',
310
+ marginBottom: 12,
311
+ padding: 4,
312
+ },
313
+ forgotPasswordText: {
314
+ color: '#007AFF',
315
+ fontSize: 14,
316
+ fontWeight: '500',
317
+ },
318
+ modeTitle: {
319
+ fontSize: 18,
320
+ fontWeight: 'bold',
321
+ marginBottom: 16,
322
+ color: '#333',
323
+ textAlign: 'center',
324
+ },
325
+ cancelButton: {
326
+ marginTop: 16,
327
+ alignItems: 'center',
328
+ padding: 8,
329
+ },
330
+ cancelButtonText: {
331
+ color: '#666',
332
+ fontSize: 14,
333
+ fontWeight: '500',
334
+ },
255
335
  });
@@ -2,22 +2,30 @@ 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
4
  import * as FirebaseAuth from 'firebase/auth';
5
+
6
+ import { sendPasswordResetEmail } from 'firebase/auth';
5
7
  import ReactNativeAsyncStorage from '@react-native-async-storage/async-storage';
6
8
  import { GoogleSignin } from '@react-native-google-signin/google-signin';
7
9
  import * as AppleAuthentication from 'expo-apple-authentication';
8
10
  import * as Crypto from 'expo-crypto';
9
- import { mapFirebaseError } from '../errors';
11
+ import {
12
+ AuthException,
13
+ mapFirebaseError,
14
+ ConfigurationException,
15
+ AppleSignInNotSupportedException,
16
+ AppleSignInCancelledException,
17
+ GoogleSignInCancelledException,
18
+ } from '../errors';
10
19
  import { AuthContext } from './AuthContext';
11
- import {
12
- AuthConfig,
13
- AuthStatus,
14
- User,
15
- AuthError,
16
- AuthErrorCode,
17
- ProviderErrorCodes,
20
+ import {
21
+ AuthConfig,
22
+ AuthStatus,
23
+ User,
24
+ ProviderErrorCodes,
18
25
  EmailSignInOptions,
19
- EmailSignUpOptions
26
+ EmailSignUpOptions
20
27
  } from '../types';
28
+
21
29
  const getReactNativePersistence = (FirebaseAuth as any).getReactNativePersistence;
22
30
 
23
31
  interface AuthProviderProps {
@@ -28,7 +36,7 @@ interface AuthProviderProps {
28
36
  export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children }) => {
29
37
  const [user, setUser] = useState<User | null>(null);
30
38
  const [status, setStatus] = useState<AuthStatus>(AuthStatus.LOADING);
31
- const [error, setError] = useState<AuthError | null>(null);
39
+ const [error, setError] = useState<AuthException | null>(null);
32
40
  const [firebaseAuthInstance, setFirebaseAuthInstance] = useState<FirebaseAuth.Auth | null>(null);
33
41
  const [isDataLoading, setIsDataLoading] = useState(true);
34
42
 
@@ -47,8 +55,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
47
55
  appId: config.appId,
48
56
  });
49
57
 
50
- const selectedPersistence = config.persistence === 'memory'
51
- ? FirebaseAuth.inMemoryPersistence
58
+ const selectedPersistence = config.persistence === 'memory'
59
+ ? FirebaseAuth.inMemoryPersistence
52
60
  : getReactNativePersistence(ReactNativeAsyncStorage);
53
61
 
54
62
  auth = FirebaseAuth.initializeAuth(app, {
@@ -66,7 +74,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
66
74
  GoogleSignin.configure({
67
75
  webClientId: config.googleWebClientId,
68
76
  offlineAccess: true,
69
- iosClientId: config.googleIOSClientId,
77
+ iosClientId: config.googleIOSClientId,
70
78
  });
71
79
  console.log('Google Sign-In configured successfully');
72
80
  } catch (err) {
@@ -90,11 +98,13 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
90
98
  setStatus(AuthStatus.AUTHENTICATED);
91
99
  } catch (tokenError: any) {
92
100
  console.error('Token retrieval error:', tokenError);
93
- if (tokenError.code === ProviderErrorCodes.USER_TOKEN_EXPIRED ||
101
+ if (tokenError.code === ProviderErrorCodes.USER_TOKEN_EXPIRED ||
94
102
  tokenError.code === ProviderErrorCodes.NULL_USER) {
95
103
  setStatus(AuthStatus.TOKEN_EXPIRED);
104
+ setError(mapFirebaseError(tokenError));
96
105
  } else {
97
106
  setStatus(AuthStatus.UNAUTHENTICATED);
107
+ setError(mapFirebaseError(tokenError));
98
108
  }
99
109
  setUser(null);
100
110
  }
@@ -105,6 +115,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
105
115
  } catch (err) {
106
116
  console.error("Auth State Error:", err);
107
117
  setStatus(AuthStatus.UNAUTHENTICATED);
118
+ setError(mapFirebaseError(err));
108
119
  } finally {
109
120
  setIsDataLoading(false);
110
121
  }
@@ -122,10 +133,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
122
133
  setStatus(AuthStatus.LOADING);
123
134
  await FirebaseAuth.signInWithEmailAndPassword(firebaseAuthInstance, email, password);
124
135
  } catch (err: any) {
125
- const mapped = mapFirebaseError(err);
126
- setError({ ...mapped, originalError: err });
136
+ const mappedException = mapFirebaseError(err);
137
+ setError(mappedException);
127
138
  setStatus(AuthStatus.UNAUTHENTICATED);
128
- throw err;
139
+ throw mappedException;
129
140
  }
130
141
  };
131
142
 
@@ -136,18 +147,41 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
136
147
  setStatus(AuthStatus.LOADING);
137
148
  await FirebaseAuth.createUserWithEmailAndPassword(firebaseAuthInstance, email, password);
138
149
  } catch (err: any) {
139
- const mapped = mapFirebaseError(err);
140
- setError({ ...mapped, originalError: err });
150
+ const mappedException = mapFirebaseError(err);
151
+ setError(mappedException);
152
+ setStatus(AuthStatus.UNAUTHENTICATED);
153
+ throw mappedException;
154
+ }
155
+ };
156
+
157
+
158
+ const sendPasswordReset = async (email: string) => {
159
+ if (!firebaseAuthInstance) return;
160
+ try {
161
+ setError(null);
162
+ setStatus(AuthStatus.LOADING);
163
+
164
+ await sendPasswordResetEmail(firebaseAuthInstance, email);
165
+
166
+
167
+ setStatus(AuthStatus.UNAUTHENTICATED);
168
+ console.log(`Password reset email sent to ${email}`);
169
+ } catch (err: any) {
170
+ const mappedException = mapFirebaseError(err);
171
+ setError(mappedException);
141
172
  setStatus(AuthStatus.UNAUTHENTICATED);
142
- throw err;
173
+ throw mappedException;
143
174
  }
144
175
  };
145
176
 
146
177
  const signInWithGoogle = async () => {
147
178
  if (!firebaseAuthInstance) throw new Error('Firebase not initialized');
148
179
  if (!config.enableGoogle || !config.googleWebClientId) {
149
- setError({ code: AuthErrorCode.CONFIG_ERROR, message: 'Google Auth not configured. Missing googleWebClientId.' });
150
- return;
180
+ const configError = new ConfigurationException(
181
+ 'Google Auth not configured. Missing googleWebClientId.'
182
+ );
183
+ setError(configError);
184
+ throw configError;
151
185
  }
152
186
 
153
187
  try {
@@ -166,56 +200,33 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
166
200
 
167
201
  } catch (err: any) {
168
202
  console.error('Google Sign-In Error:', err);
169
- let mappedError: AuthError;
170
203
 
171
204
  if (err.code === ProviderErrorCodes.GOOGLE_CANCELLED) {
172
- mappedError = {
173
- code: AuthErrorCode.GOOGLE_SIGN_IN_CANCELLED,
174
- message: 'Google Sign-In was cancelled',
175
- originalError: err
176
- };
177
- setStatus(AuthStatus.UNAUTHENTICATED);
178
- return;
179
- } else if (err.code === ProviderErrorCodes.GOOGLE_IN_PROGRESS) {
180
- mappedError = {
181
- code: AuthErrorCode.GOOGLE_SIGN_IN_IN_PROGRESS,
182
- message: 'Google Sign-In is already in progress',
183
- originalError: err
184
- };
185
- } else if (err.code === ProviderErrorCodes.GOOGLE_PLAY_UNAVAILABLE) {
186
- mappedError = {
187
- code: AuthErrorCode.GOOGLE_PLAY_SERVICES_NOT_AVAILABLE,
188
- message: 'Google Play Services are not available. Please update Google Play Services.',
189
- originalError: err
190
- };
191
- } else {
192
- mappedError = mapFirebaseError(err);
205
+ const cancelError = new GoogleSignInCancelledException(err);
206
+ setError(cancelError);
207
+ setStatus(AuthStatus.UNAUTHENTICATED);
208
+ return;
193
209
  }
194
210
 
195
- setError({ ...mappedError, originalError: err });
211
+ const mappedException = mapFirebaseError(err);
212
+ setError(mappedException);
196
213
  setStatus(AuthStatus.UNAUTHENTICATED);
197
- throw mappedError;
214
+ throw mappedException;
198
215
  }
199
216
  };
200
217
 
201
218
  const signInWithApple = async () => {
202
219
  if (!firebaseAuthInstance) throw new Error('Firebase not initialized');
203
-
220
+
204
221
  if (Platform.OS !== 'ios') {
205
- const platformError: AuthError = {
206
- code: AuthErrorCode.APPLE_SIGN_IN_NOT_SUPPORTED,
207
- message: 'Apple Sign-In is only available on iOS devices',
208
- };
222
+ const platformError = new AppleSignInNotSupportedException();
209
223
  setError(platformError);
210
224
  throw platformError;
211
225
  }
212
226
 
213
227
  const isAvailable = await AppleAuthentication.isAvailableAsync();
214
228
  if (!isAvailable) {
215
- const availabilityError: AuthError = {
216
- code: AuthErrorCode.APPLE_SIGN_IN_NOT_SUPPORTED,
217
- message: 'Apple Sign-In is not available on this device (requires iOS 13+)',
218
- };
229
+ const availabilityError = new AppleSignInNotSupportedException();
219
230
  setError(availabilityError);
220
231
  throw availabilityError;
221
232
  }
@@ -223,10 +234,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
223
234
  try {
224
235
  setError(null);
225
236
  setStatus(AuthStatus.LOADING);
226
-
237
+
227
238
  const nonce = Math.random().toString(36).substring(2, 10);
228
239
  const hashedNonce = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, nonce);
229
-
240
+
230
241
  const appleCredential = await AppleAuthentication.signInAsync({
231
242
  requestedScopes: [
232
243
  AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
@@ -252,20 +263,16 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
252
263
  console.error('Apple Sign-In Error:', err);
253
264
 
254
265
  if (err.code === ProviderErrorCodes.APPLE_CANCELLED) {
255
- const cancelError: AuthError = {
256
- code: AuthErrorCode.APPLE_SIGN_IN_CANCELLED,
257
- message: 'Apple Sign-In was cancelled',
258
- originalError: err
259
- };
266
+ const cancelError = new AppleSignInCancelledException(err);
260
267
  setError(cancelError);
261
268
  setStatus(AuthStatus.UNAUTHENTICATED);
262
269
  return;
263
270
  }
264
271
 
265
- const mappedError = mapFirebaseError(err);
266
- setError({ ...mappedError, originalError: err });
272
+ const mappedException = mapFirebaseError(err);
273
+ setError(mappedException);
267
274
  setStatus(AuthStatus.UNAUTHENTICATED);
268
- throw mappedError;
275
+ throw mappedException;
269
276
  }
270
277
  };
271
278
 
@@ -284,6 +291,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
284
291
  console.log('Sign out successful');
285
292
  } catch (err) {
286
293
  console.error('Sign out error:', err);
294
+ const signOutError = mapFirebaseError(err);
295
+ setError(signOutError);
287
296
  setUser(null);
288
297
  setStatus(AuthStatus.UNAUTHENTICATED);
289
298
  }
@@ -299,6 +308,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
299
308
  config,
300
309
  signInWithEmail,
301
310
  signUpWithEmail,
311
+ sendPasswordReset,
302
312
  signInWithGoogle,
303
313
  signInWithApple,
304
314
  signOut,