rn-swiftauth-sdk 1.0.3 → 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.
- package/README.md +115 -1
- package/dist/components/LoginForm.js +74 -29
- package/dist/core/AuthProvider.js +19 -0
- package/dist/types/auth.types.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/LoginForm.tsx +128 -48
- package/src/core/AuthProvider.tsx +23 -0
- package/src/types/auth.types.ts +1 -1
package/README.md
CHANGED
|
@@ -231,6 +231,120 @@ const {
|
|
|
231
231
|
buttonText: { color: '#000000', fontWeight: 'bold' }
|
|
232
232
|
}}
|
|
233
233
|
/>
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 🔐 Password Management
|
|
240
|
+
|
|
241
|
+
The **SwiftAuth SDK** includes built-in support for **password recovery flows** using **Firebase’s email-based password reset mechanism**.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### ✉️ Sending a Reset Email
|
|
246
|
+
|
|
247
|
+
The `sendPasswordReset(email)` function sends a password reset link to the user’s email address.
|
|
248
|
+
|
|
249
|
+
#### Method Signature
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
sendPasswordReset: (email: string) => Promise<void>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### 📌 Usage Example (React Native)
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import React, { useState } from 'react';
|
|
261
|
+
import { View, TextInput, Button, Alert, Text } from 'react-native';
|
|
262
|
+
import { useAuth } from 'rn-swiftauth-sdk';
|
|
263
|
+
|
|
264
|
+
export const ForgotPasswordScreen = () => {
|
|
265
|
+
const [email, setEmail] = useState('');
|
|
266
|
+
const { sendPasswordReset, isLoading, error } = useAuth();
|
|
267
|
+
|
|
268
|
+
const handleReset = async () => {
|
|
269
|
+
if (!email) {
|
|
270
|
+
Alert.alert("Error", "Please enter your email address.");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await sendPasswordReset(email);
|
|
276
|
+
Alert.alert("Success", "Password reset link sent! Check your email.");
|
|
277
|
+
} catch (err) {
|
|
278
|
+
// Errors are automatically handled by the global `error` state,
|
|
279
|
+
// but can also be caught locally for custom logic.
|
|
280
|
+
console.error("Reset failed:", err);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<View>
|
|
286
|
+
<TextInput
|
|
287
|
+
placeholder="Enter your email"
|
|
288
|
+
value={email}
|
|
289
|
+
onChangeText={setEmail}
|
|
290
|
+
autoCapitalize="none"
|
|
291
|
+
keyboardType="email-address"
|
|
292
|
+
/>
|
|
293
|
+
|
|
294
|
+
<Button
|
|
295
|
+
title={isLoading ? "Sending..." : "Reset Password"}
|
|
296
|
+
onPress={handleReset}
|
|
297
|
+
disabled={isLoading}
|
|
298
|
+
/>
|
|
299
|
+
|
|
300
|
+
{error && <Text style={{ color: 'red' }}>{error.message}</Text>}
|
|
301
|
+
</View>
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### ⚠️ Error Handling
|
|
309
|
+
|
|
310
|
+
The error handling system includes **specific exceptions** for password reset flows.
|
|
311
|
+
|
|
312
|
+
| Exception Class | Error Code | User Message |
|
|
313
|
+
| ----------------------- | --------------------- | ------------------------------------------------------------------- |
|
|
314
|
+
| `UserNotFoundException` | `auth/user-not-found` | "No account found with this email. Please check the email address." |
|
|
315
|
+
| `InvalidEmailException` | `auth/invalid-email` | "The email address is badly formatted." |
|
|
316
|
+
|
|
317
|
+
Errors are exposed through the global `error` state returned by `useAuth()` and can also be handled locally using `try/catch`.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
### 🎨 Integration with Custom UI
|
|
322
|
+
|
|
323
|
+
You can integrate password recovery into **custom login forms** by toggling a “Forgot Password” mode using the headless hook.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const { sendPasswordReset } = useAuth();
|
|
327
|
+
|
|
328
|
+
const onForgotPasswordPress = async (email: string) => {
|
|
329
|
+
await sendPasswordReset(email);
|
|
330
|
+
// Show success feedback to the user
|
|
331
|
+
};
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
This allows you to fully control your UI while leveraging SwiftAuth’s secure password reset flow.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
If you want, I can also:
|
|
339
|
+
|
|
340
|
+
* Add **UX flow diagrams**
|
|
341
|
+
* Add **Firebase setup prerequisites**
|
|
342
|
+
* Document **rate limits & edge cases**
|
|
343
|
+
* Align tone with the rest of your SDK README
|
|
344
|
+
|
|
345
|
+
Just tell me 👍
|
|
346
|
+
|
|
347
|
+
|
|
234
348
|
```
|
|
235
349
|
|
|
236
350
|
---
|
|
@@ -499,7 +613,7 @@ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
|
499
613
|
|
|
500
614
|
## License
|
|
501
615
|
|
|
502
|
-
MIT License - see [LICENSE](LICENSE) file for
|
|
616
|
+
MIT License - see [LICENSE](LICENSE) file for detailss.
|
|
503
617
|
|
|
504
618
|
---
|
|
505
619
|
|
|
@@ -40,16 +40,35 @@ const useAuth_1 = require("../hooks/useAuth");
|
|
|
40
40
|
const PasswordInput_1 = require("./PasswordInput");
|
|
41
41
|
const validation_1 = require("../utils/validation");
|
|
42
42
|
const LoginForm = ({ styles: userStyles }) => {
|
|
43
|
-
const { signInWithEmail, signInWithGoogle, signInWithApple, isLoading, error, config } = (0, useAuth_1.useAuth)();
|
|
43
|
+
const { signInWithEmail, signInWithGoogle, signInWithApple, sendPasswordReset, isLoading, error, config, clearError } = (0, useAuth_1.useAuth)();
|
|
44
44
|
const [email, setEmail] = (0, react_1.useState)('');
|
|
45
45
|
const [password, setPassword] = (0, react_1.useState)('');
|
|
46
|
+
const [isResetMode, setIsResetMode] = (0, react_1.useState)(false); // ✅ 2. Mode Toggle State
|
|
46
47
|
const [validationErrors, setValidationErrors] = (0, react_1.useState)({});
|
|
47
|
-
//
|
|
48
|
-
const isFormFilled =
|
|
48
|
+
// ✅ 3. Dynamic check: If resetting, we only need Email. If logging in, we need both.
|
|
49
|
+
const isFormFilled = isResetMode
|
|
50
|
+
? email.length > 0
|
|
51
|
+
: (email.length > 0 && password.length > 0);
|
|
52
|
+
// ✅ 4. New Handler for Password Reset
|
|
53
|
+
const handleResetPassword = async () => {
|
|
54
|
+
setValidationErrors({});
|
|
55
|
+
const emailErr = (0, validation_1.validateEmail)(email);
|
|
56
|
+
if (emailErr) {
|
|
57
|
+
setValidationErrors({ email: emailErr });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
await sendPasswordReset(email);
|
|
62
|
+
react_native_1.Alert.alert("Check your email", "If an account exists with this email, a password reset link has been sent.");
|
|
63
|
+
setIsResetMode(false); // Go back to login screen
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
console.log('Reset failed:', e);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
49
69
|
const handleLogin = async () => {
|
|
50
|
-
// 1. Reset previous errors
|
|
51
70
|
setValidationErrors({});
|
|
52
|
-
//
|
|
71
|
+
// Validate Inputs
|
|
53
72
|
const emailErr = (0, validation_1.validateEmail)(email);
|
|
54
73
|
const passErr = (0, validation_1.validatePasswordLogin)(password);
|
|
55
74
|
if (emailErr || passErr) {
|
|
@@ -57,16 +76,12 @@ const LoginForm = ({ styles: userStyles }) => {
|
|
|
57
76
|
email: emailErr || undefined,
|
|
58
77
|
password: passErr || undefined
|
|
59
78
|
});
|
|
60
|
-
return;
|
|
79
|
+
return;
|
|
61
80
|
}
|
|
62
|
-
// 3. Attempt Login
|
|
63
81
|
try {
|
|
64
|
-
//UPDATED: Clean Object Syntax
|
|
65
82
|
await signInWithEmail({ email, password });
|
|
66
83
|
}
|
|
67
84
|
catch (e) {
|
|
68
|
-
// Auth errors handled by global state
|
|
69
|
-
// DX: Log it for the developer (Optional but helpful for debugging)
|
|
70
85
|
console.log('Login failed:', e);
|
|
71
86
|
}
|
|
72
87
|
};
|
|
@@ -86,8 +101,15 @@ const LoginForm = ({ styles: userStyles }) => {
|
|
|
86
101
|
console.error('Apple Sign-In Error:', e);
|
|
87
102
|
}
|
|
88
103
|
};
|
|
104
|
+
const toggleResetMode = () => {
|
|
105
|
+
setIsResetMode(!isResetMode);
|
|
106
|
+
clearError();
|
|
107
|
+
setValidationErrors({});
|
|
108
|
+
setPassword('');
|
|
109
|
+
};
|
|
89
110
|
return (react_1.default.createElement(react_native_1.View, { style: [defaultStyles.container, userStyles?.container] },
|
|
90
111
|
error && (react_1.default.createElement(react_native_1.Text, { style: [defaultStyles.errorText, userStyles?.errorText] }, error.message)),
|
|
112
|
+
isResetMode && (react_1.default.createElement(react_native_1.Text, { style: defaultStyles.modeTitle }, "Reset Password")),
|
|
91
113
|
react_1.default.createElement(react_native_1.TextInput, { style: [
|
|
92
114
|
defaultStyles.input,
|
|
93
115
|
userStyles?.input,
|
|
@@ -98,34 +120,30 @@ const LoginForm = ({ styles: userStyles }) => {
|
|
|
98
120
|
setValidationErrors({ ...validationErrors, email: undefined });
|
|
99
121
|
}, autoCapitalize: "none", keyboardType: "email-address", placeholderTextColor: "#999", editable: !isLoading }),
|
|
100
122
|
validationErrors.email && (react_1.default.createElement(react_native_1.Text, { style: defaultStyles.validationText }, validationErrors.email)),
|
|
101
|
-
react_1.default.createElement(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
!isResetMode && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
124
|
+
react_1.default.createElement(PasswordInput_1.PasswordInput, { styles: userStyles, placeholder: "Password", value: password, onChangeText: (text) => {
|
|
125
|
+
setPassword(text);
|
|
126
|
+
if (validationErrors.password)
|
|
127
|
+
setValidationErrors({ ...validationErrors, password: undefined });
|
|
128
|
+
}, editable: !isLoading }),
|
|
129
|
+
validationErrors.password && (react_1.default.createElement(react_native_1.Text, { style: defaultStyles.validationText }, validationErrors.password)),
|
|
130
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: defaultStyles.forgotPasswordContainer, onPress: toggleResetMode, disabled: isLoading },
|
|
131
|
+
react_1.default.createElement(react_native_1.Text, { style: [defaultStyles.forgotPasswordText, userStyles?.linkText] }, "Forgot Password?")))),
|
|
107
132
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
108
133
|
defaultStyles.button,
|
|
109
|
-
// Disable style if loading OR form is incomplete
|
|
110
134
|
(isLoading || !isFormFilled) && defaultStyles.buttonDisabled,
|
|
111
135
|
userStyles?.button
|
|
112
|
-
], onPress: handleLogin,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
(config.enableGoogle || config.enableApple) && !isLoading && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
136
|
+
], onPress: isResetMode ? handleResetPassword : handleLogin, disabled: isLoading || !isFormFilled }, isLoading ? (react_1.default.createElement(react_native_1.ActivityIndicator, { color: userStyles?.loadingIndicatorColor || "#fff" })) : (react_1.default.createElement(react_native_1.Text, { style: [defaultStyles.buttonText, userStyles?.buttonText] }, isResetMode ? "Send Reset Link" : "Sign In"))),
|
|
137
|
+
isResetMode && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: defaultStyles.cancelButton, onPress: toggleResetMode, disabled: isLoading },
|
|
138
|
+
react_1.default.createElement(react_native_1.Text, { style: defaultStyles.cancelButtonText }, "Back to Sign In"))),
|
|
139
|
+
!isResetMode && (config.enableGoogle || config.enableApple) && !isLoading && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
116
140
|
react_1.default.createElement(react_native_1.View, { style: defaultStyles.dividerContainer },
|
|
117
141
|
react_1.default.createElement(react_native_1.View, { style: defaultStyles.divider }),
|
|
118
142
|
react_1.default.createElement(react_native_1.Text, { style: defaultStyles.dividerText }, "OR"),
|
|
119
143
|
react_1.default.createElement(react_native_1.View, { style: defaultStyles.divider })),
|
|
120
|
-
config.enableGoogle && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
121
|
-
defaultStyles.oauthButton,
|
|
122
|
-
defaultStyles.googleButton,
|
|
123
|
-
], onPress: handleGoogleSignIn },
|
|
144
|
+
config.enableGoogle && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [defaultStyles.oauthButton, defaultStyles.googleButton], onPress: handleGoogleSignIn },
|
|
124
145
|
react_1.default.createElement(react_native_1.Text, { style: defaultStyles.googleButtonText }, "Continue with Google"))),
|
|
125
|
-
config.enableApple && react_native_1.Platform.OS === 'ios' && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
126
|
-
defaultStyles.oauthButton,
|
|
127
|
-
defaultStyles.appleButton,
|
|
128
|
-
], onPress: handleAppleSignIn },
|
|
146
|
+
config.enableApple && react_native_1.Platform.OS === 'ios' && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [defaultStyles.oauthButton, defaultStyles.appleButton], onPress: handleAppleSignIn },
|
|
129
147
|
react_1.default.createElement(react_native_1.Text, { style: defaultStyles.appleButtonText }, "Continue with Apple")))))));
|
|
130
148
|
};
|
|
131
149
|
exports.LoginForm = LoginForm;
|
|
@@ -185,4 +203,31 @@ const defaultStyles = react_native_1.StyleSheet.create({
|
|
|
185
203
|
fontSize: 16,
|
|
186
204
|
fontWeight: '600',
|
|
187
205
|
},
|
|
206
|
+
forgotPasswordContainer: {
|
|
207
|
+
alignSelf: 'flex-end',
|
|
208
|
+
marginBottom: 12,
|
|
209
|
+
padding: 4,
|
|
210
|
+
},
|
|
211
|
+
forgotPasswordText: {
|
|
212
|
+
color: '#007AFF',
|
|
213
|
+
fontSize: 14,
|
|
214
|
+
fontWeight: '500',
|
|
215
|
+
},
|
|
216
|
+
modeTitle: {
|
|
217
|
+
fontSize: 18,
|
|
218
|
+
fontWeight: 'bold',
|
|
219
|
+
marginBottom: 16,
|
|
220
|
+
color: '#333',
|
|
221
|
+
textAlign: 'center',
|
|
222
|
+
},
|
|
223
|
+
cancelButton: {
|
|
224
|
+
marginTop: 16,
|
|
225
|
+
alignItems: 'center',
|
|
226
|
+
padding: 8,
|
|
227
|
+
},
|
|
228
|
+
cancelButtonText: {
|
|
229
|
+
color: '#666',
|
|
230
|
+
fontSize: 14,
|
|
231
|
+
fontWeight: '500',
|
|
232
|
+
},
|
|
188
233
|
});
|
|
@@ -41,6 +41,7 @@ const react_1 = __importStar(require("react"));
|
|
|
41
41
|
const react_native_1 = require("react-native");
|
|
42
42
|
const app_1 = require("firebase/app");
|
|
43
43
|
const FirebaseAuth = __importStar(require("firebase/auth"));
|
|
44
|
+
const auth_1 = require("firebase/auth");
|
|
44
45
|
const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
|
|
45
46
|
const google_signin_1 = require("@react-native-google-signin/google-signin");
|
|
46
47
|
const AppleAuthentication = __importStar(require("expo-apple-authentication"));
|
|
@@ -169,6 +170,23 @@ const AuthProvider = ({ config, children }) => {
|
|
|
169
170
|
throw mappedException;
|
|
170
171
|
}
|
|
171
172
|
};
|
|
173
|
+
const sendPasswordReset = async (email) => {
|
|
174
|
+
if (!firebaseAuthInstance)
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
setError(null);
|
|
178
|
+
setStatus(types_1.AuthStatus.LOADING);
|
|
179
|
+
await (0, auth_1.sendPasswordResetEmail)(firebaseAuthInstance, email);
|
|
180
|
+
setStatus(types_1.AuthStatus.UNAUTHENTICATED);
|
|
181
|
+
console.log(`Password reset email sent to ${email}`);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const mappedException = (0, errors_1.mapFirebaseError)(err);
|
|
185
|
+
setError(mappedException);
|
|
186
|
+
setStatus(types_1.AuthStatus.UNAUTHENTICATED);
|
|
187
|
+
throw mappedException;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
172
190
|
const signInWithGoogle = async () => {
|
|
173
191
|
if (!firebaseAuthInstance)
|
|
174
192
|
throw new Error('Firebase not initialized');
|
|
@@ -286,6 +304,7 @@ const AuthProvider = ({ config, children }) => {
|
|
|
286
304
|
config,
|
|
287
305
|
signInWithEmail,
|
|
288
306
|
signUpWithEmail,
|
|
307
|
+
sendPasswordReset,
|
|
289
308
|
signInWithGoogle,
|
|
290
309
|
signInWithApple,
|
|
291
310
|
signOut,
|
|
@@ -30,6 +30,7 @@ export interface AuthContextType {
|
|
|
30
30
|
config: AuthConfig;
|
|
31
31
|
signInWithEmail: (options: EmailSignInOptions) => Promise<void>;
|
|
32
32
|
signUpWithEmail: (options: EmailSignUpOptions) => Promise<void>;
|
|
33
|
+
sendPasswordReset: (email: string) => Promise<void>;
|
|
33
34
|
signInWithGoogle: () => Promise<void>;
|
|
34
35
|
signInWithApple: () => Promise<void>;
|
|
35
36
|
signOut: () => Promise<void>;
|
package/package.json
CHANGED
|
@@ -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
|
-
//
|
|
36
|
-
const isFormFilled =
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
114
|
+
|
|
84
115
|
{error && (
|
|
85
116
|
<Text style={[defaultStyles.errorText, userStyles?.errorText]}>
|
|
86
117
|
{error.message}
|
|
87
118
|
</Text>
|
|
88
119
|
)}
|
|
89
120
|
|
|
90
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
149
|
-
{
|
|
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,6 +2,8 @@ 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';
|
|
@@ -152,6 +154,26 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
152
154
|
}
|
|
153
155
|
};
|
|
154
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);
|
|
172
|
+
setStatus(AuthStatus.UNAUTHENTICATED);
|
|
173
|
+
throw mappedException;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
155
177
|
const signInWithGoogle = async () => {
|
|
156
178
|
if (!firebaseAuthInstance) throw new Error('Firebase not initialized');
|
|
157
179
|
if (!config.enableGoogle || !config.googleWebClientId) {
|
|
@@ -286,6 +308,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ config, children })
|
|
|
286
308
|
config,
|
|
287
309
|
signInWithEmail,
|
|
288
310
|
signUpWithEmail,
|
|
311
|
+
sendPasswordReset,
|
|
289
312
|
signInWithGoogle,
|
|
290
313
|
signInWithApple,
|
|
291
314
|
signOut,
|
package/src/types/auth.types.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface AuthContextType {
|
|
|
38
38
|
// Function Signatures
|
|
39
39
|
signInWithEmail: (options: EmailSignInOptions) => Promise<void>;
|
|
40
40
|
signUpWithEmail: (options: EmailSignUpOptions) => Promise<void>;
|
|
41
|
-
|
|
41
|
+
sendPasswordReset: (email: string) => Promise<void>;
|
|
42
42
|
signInWithGoogle: () => Promise<void>;
|
|
43
43
|
signInWithApple: () => Promise<void>;
|
|
44
44
|
signOut: () => Promise<void>;
|