@umituz/react-native-auth 3.6.6 → 3.6.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-auth",
3
- "version": "3.6.6",
3
+ "version": "3.6.7",
4
4
  "description": "Authentication service for React Native apps - Secure, type-safe, and production-ready. Provider-agnostic design with dependency injection, configurable validation, and comprehensive error handling.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/index.ts CHANGED
@@ -161,6 +161,8 @@ export { AccountScreen } from './presentation/screens/AccountScreen';
161
161
  export type { AccountScreenConfig, AccountScreenProps } from './presentation/screens/AccountScreen';
162
162
  export { EditProfileScreen } from './presentation/screens/EditProfileScreen';
163
163
  export type { EditProfileConfig, EditProfileScreenProps } from './presentation/screens/EditProfileScreen';
164
+ export { ChangePasswordScreen } from './presentation/screens/ChangePasswordScreen';
165
+ export type { ChangePasswordScreenProps } from './presentation/screens/ChangePasswordScreen';
164
166
  export { AuthNavigator } from './presentation/navigation/AuthNavigator';
165
167
  export type {
166
168
  AuthStackParamList,
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Change Password Screen
3
+ * Screen for users to update their password
4
+ *
5
+ * Features:
6
+ * - Current password validation via re-authentication
7
+ * - New password validation (strength, match)
8
+ * - Secure error handling
9
+ */
10
+
11
+ import React, { useState } from "react";
12
+ import { View, StyleSheet, Alert } from "react-native";
13
+ import {
14
+ ScreenLayout,
15
+ AtomicInput,
16
+ AtomicButton,
17
+ AtomicText,
18
+ useAppDesignTokens,
19
+ type AtomicInputProps,
20
+ } from "@umituz/react-native-design-system";
21
+ import { useTranslation } from "@umituz/react-native-localization";
22
+ import {
23
+ updateUserPassword,
24
+ reauthenticateWithPassword,
25
+ getCurrentUserFromGlobal,
26
+ getFirebaseAuth,
27
+ } from "@umituz/react-native-firebase";
28
+
29
+ export interface ChangePasswordScreenProps {
30
+ onSuccess?: () => void;
31
+ onCancel?: () => void;
32
+ }
33
+
34
+ export const ChangePasswordScreen: React.FC<ChangePasswordScreenProps> = ({
35
+ onSuccess,
36
+ onCancel,
37
+ }) => {
38
+ const { t } = useTranslation();
39
+ const tokens = useAppDesignTokens();
40
+ const auth = getFirebaseAuth();
41
+
42
+ const [currentPassword, setCurrentPassword] = useState("");
43
+ const [newPassword, setNewPassword] = useState("");
44
+ const [confirmPassword, setConfirmPassword] = useState("");
45
+ const [loading, setLoading] = useState(false);
46
+ const [error, setError] = useState<string | null>(null);
47
+
48
+ const [showCurrentPassword, setShowCurrentPassword] = useState(false);
49
+ const [showNewPassword, setShowNewPassword] = useState(false);
50
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
51
+
52
+ // Validation state
53
+ const isLengthValid = newPassword.length >= 8;
54
+ const hasUppercase = /[A-Z]/.test(newPassword);
55
+ const hasLowercase = /[a-z]/.test(newPassword);
56
+ const hasNumber = /[0-9]/.test(newPassword);
57
+ const hasSpecialChar = /[!@#$%^&*]/.test(newPassword);
58
+ const passwordsMatch = newPassword === confirmPassword && newPassword !== "";
59
+
60
+ const isValid =
61
+ isLengthValid &&
62
+ hasUppercase &&
63
+ hasLowercase &&
64
+ hasNumber &&
65
+ hasSpecialChar &&
66
+ passwordsMatch &&
67
+ currentPassword.length > 0;
68
+
69
+ const handleChangePassword = async () => {
70
+ if (!isValid) {
71
+ Alert.alert(t("common.error"), t("auth.passwordChange.fillAllFields"));
72
+ return;
73
+ }
74
+
75
+ const user = getCurrentUserFromGlobal();
76
+ if (!user) {
77
+ setError(t("auth.errors.unauthorized"));
78
+ return;
79
+ }
80
+
81
+ setLoading(true);
82
+ setError(null);
83
+
84
+ try {
85
+ // 1. Re-authenticate
86
+ const reauthResult = await reauthenticateWithPassword(user, currentPassword);
87
+ if (!reauthResult.success) {
88
+ setError(reauthResult.error?.message || t("auth.alerts.error.signInFailed"));
89
+ setLoading(false);
90
+ return;
91
+ }
92
+
93
+ // 2. Update Password
94
+ const updateResult = await updateUserPassword(user, newPassword);
95
+ if (updateResult.success) {
96
+ Alert.alert(t("common.success"), t("auth.passwordChange.success"), [
97
+ { text: "OK", onPress: onSuccess }
98
+ ]);
99
+ // Iterate onSuccess callback as well in case alert is dismissed or handled differently in native
100
+ if (onSuccess) onSuccess();
101
+ } else {
102
+ setError(updateResult.error?.message || t("auth.passwordChange.error"));
103
+ }
104
+ } catch (e: any) {
105
+ setError(e.message || t("auth.passwordChange.error"));
106
+ } finally {
107
+ setLoading(false);
108
+ }
109
+ };
110
+
111
+ const RequirementsList = () => (
112
+ <View style={styles.requirementsContainer}>
113
+ <AtomicText variant="labelMedium" style={{ color: tokens.colors.textSecondary, marginBottom: 8 }}>
114
+ {t("auth.passwordChange.requirements")}
115
+ </AtomicText>
116
+ <RequirementItem label={t("auth.passwordChange.minLength")} met={isLengthValid} />
117
+ <RequirementItem label={t("auth.passwordChange.uppercase")} met={hasUppercase} />
118
+ <RequirementItem label={t("auth.passwordChange.lowercase")} met={hasLowercase} />
119
+ <RequirementItem label={t("auth.passwordChange.number")} met={hasNumber} />
120
+ <RequirementItem label={t("auth.passwordChange.specialChar")} met={hasSpecialChar} />
121
+ <RequirementItem label={t("auth.passwordChange.passwordsMatch")} met={passwordsMatch} />
122
+ </View>
123
+ );
124
+
125
+ const RequirementItem = ({ label, met }: { label: string; met: boolean }) => (
126
+ <View style={styles.requirementItem}>
127
+ <View
128
+ style={[
129
+ styles.requirementBullet,
130
+ { backgroundColor: met ? tokens.colors.success : tokens.colors.border },
131
+ ]}
132
+ />
133
+ <AtomicText
134
+ variant="bodySmall"
135
+ style={{ color: met ? tokens.colors.textPrimary : tokens.colors.textSecondary }}
136
+ >
137
+ {label}
138
+ </AtomicText>
139
+ </View>
140
+ );
141
+
142
+ return (
143
+ <ScreenLayout
144
+ scrollable
145
+ title={t("auth.passwordChange.title")}
146
+ backgroundColor={tokens.colors.backgroundPrimary}
147
+ edges={["bottom"]}
148
+ >
149
+ <View style={styles.content}>
150
+ <AtomicText variant="bodyMedium" style={{ color: tokens.colors.textSecondary, marginBottom: 24 }}>
151
+ {t("auth.passwordChange.description")}
152
+ </AtomicText>
153
+
154
+ <View style={styles.form}>
155
+ {/* Current Password */}
156
+ <AtomicInput
157
+ label={t("auth.passwordChange.currentPassword")}
158
+ placeholder={t("auth.passwordChange.enterCurrentPassword")}
159
+ value={currentPassword}
160
+ onChangeText={setCurrentPassword}
161
+ secureTextEntry={!showCurrentPassword}
162
+ rightIcon={showCurrentPassword ? "eye-off" : "eye"}
163
+ onRightIconPress={() => setShowCurrentPassword(!showCurrentPassword)}
164
+ variant="filled"
165
+ />
166
+
167
+ {/* New Password */}
168
+ <AtomicInput
169
+ label={t("auth.passwordChange.newPassword")}
170
+ placeholder={t("auth.passwordChange.enterNewPassword")}
171
+ value={newPassword}
172
+ onChangeText={setNewPassword}
173
+ secureTextEntry={!showNewPassword}
174
+ rightIcon={showNewPassword ? "eye-off" : "eye"}
175
+ onRightIconPress={() => setShowNewPassword(!showNewPassword)}
176
+ variant="filled"
177
+ />
178
+
179
+ {/* Confirm Password */}
180
+ <AtomicInput
181
+ label={t("auth.passwordChange.confirmPassword")}
182
+ placeholder={t("auth.passwordChange.enterConfirmPassword")}
183
+ value={confirmPassword}
184
+ onChangeText={setConfirmPassword}
185
+ secureTextEntry={!showConfirmPassword}
186
+ rightIcon={showConfirmPassword ? "eye-off" : "eye"}
187
+ onRightIconPress={() => setShowConfirmPassword(!showConfirmPassword)}
188
+ variant="filled"
189
+ />
190
+
191
+ <RequirementsList />
192
+
193
+ {error && (
194
+ <AtomicText style={{ color: tokens.colors.error, marginTop: 16 }}>
195
+ {error}
196
+ </AtomicText>
197
+ )}
198
+
199
+ <View style={styles.actions}>
200
+ <AtomicButton
201
+ title={t("common.cancel")}
202
+ onPress={onCancel}
203
+ variant="outline"
204
+ style={{ flex: 1 }}
205
+ />
206
+ <AtomicButton
207
+ title={loading ? t("auth.passwordChange.changing") : t("auth.passwordChange.changePassword")}
208
+ onPress={handleChangePassword}
209
+ loading={loading}
210
+ disabled={!isValid || loading}
211
+ style={{ flex: 1 }}
212
+ />
213
+ </View>
214
+ </View>
215
+ </View>
216
+ </ScreenLayout>
217
+ );
218
+ };
219
+
220
+ const styles = StyleSheet.create({
221
+ content: {
222
+ padding: 16,
223
+ },
224
+ form: {
225
+ gap: 16,
226
+ },
227
+ requirementsContainer: {
228
+ marginTop: 8,
229
+ padding: 12,
230
+ borderRadius: 8,
231
+ backgroundColor: "rgba(0,0,0,0.05)",
232
+ },
233
+ requirementItem: {
234
+ flexDirection: "row",
235
+ alignItems: "center",
236
+ marginBottom: 4,
237
+ },
238
+ requirementBullet: {
239
+ width: 6,
240
+ height: 6,
241
+ borderRadius: 3,
242
+ marginRight: 8,
243
+ },
244
+ actions: {
245
+ flexDirection: "row",
246
+ gap: 12,
247
+ marginTop: 24,
248
+ },
249
+ });