@umituz/react-native-auth 1.11.0 → 1.12.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/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/presentation/components/PasswordMatchIndicator.tsx +50 -0
- package/src/presentation/components/PasswordStrengthIndicator.tsx +118 -0
- package/src/presentation/components/RegisterForm.tsx +10 -0
- package/src/presentation/hooks/useRegisterForm.ts +24 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Authentication service for React Native apps - Secure, type-safe, and production-ready. Provider-agnostic design supports Firebase Auth and can be adapted for Supabase or other providers.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -122,6 +122,10 @@ export { LoginForm } from './presentation/components/LoginForm';
|
|
|
122
122
|
export { RegisterForm } from './presentation/components/RegisterForm';
|
|
123
123
|
export { AuthLegalLinks } from './presentation/components/AuthLegalLinks';
|
|
124
124
|
export type { AuthLegalLinksProps } from './presentation/components/AuthLegalLinks';
|
|
125
|
+
export { PasswordStrengthIndicator } from './presentation/components/PasswordStrengthIndicator';
|
|
126
|
+
export type { PasswordStrengthIndicatorProps } from './presentation/components/PasswordStrengthIndicator';
|
|
127
|
+
export { PasswordMatchIndicator } from './presentation/components/PasswordMatchIndicator';
|
|
128
|
+
export type { PasswordMatchIndicatorProps } from './presentation/components/PasswordMatchIndicator';
|
|
125
129
|
|
|
126
130
|
// =============================================================================
|
|
127
131
|
// PRESENTATION LAYER - Utilities
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Match Indicator Component
|
|
3
|
+
* Shows whether passwords match
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
10
|
+
|
|
11
|
+
export interface PasswordMatchIndicatorProps {
|
|
12
|
+
isMatch: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const PasswordMatchIndicator: React.FC<PasswordMatchIndicatorProps> = ({
|
|
16
|
+
isMatch,
|
|
17
|
+
}) => {
|
|
18
|
+
const tokens = useAppDesignTokens();
|
|
19
|
+
const { t } = useLocalization();
|
|
20
|
+
|
|
21
|
+
const color = isMatch ? tokens.colors.success : tokens.colors.error;
|
|
22
|
+
const text = isMatch
|
|
23
|
+
? t("auth.passwordsMatch", "Passwords match")
|
|
24
|
+
: t("auth.passwordsDontMatch", "Passwords don't match");
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<View style={styles.container}>
|
|
28
|
+
<View style={[styles.dot, { backgroundColor: color }]} />
|
|
29
|
+
<Text style={[styles.text, { color }]}>{text}</Text>
|
|
30
|
+
</View>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const styles = StyleSheet.create({
|
|
35
|
+
container: {
|
|
36
|
+
flexDirection: "row",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
gap: 6,
|
|
39
|
+
marginTop: 8,
|
|
40
|
+
},
|
|
41
|
+
dot: {
|
|
42
|
+
width: 6,
|
|
43
|
+
height: 6,
|
|
44
|
+
borderRadius: 3,
|
|
45
|
+
},
|
|
46
|
+
text: {
|
|
47
|
+
fontSize: 12,
|
|
48
|
+
fontWeight: "500",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Strength Indicator Component
|
|
3
|
+
* Shows password requirements with visual feedback
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
import type { PasswordRequirements } from "../../infrastructure/utils/AuthValidation";
|
|
10
|
+
|
|
11
|
+
export interface PasswordStrengthIndicatorProps {
|
|
12
|
+
requirements: PasswordRequirements;
|
|
13
|
+
showLabels?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RequirementDotProps {
|
|
17
|
+
label: string;
|
|
18
|
+
isValid: boolean;
|
|
19
|
+
successColor: string;
|
|
20
|
+
pendingColor: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const RequirementDot: React.FC<RequirementDotProps> = ({
|
|
24
|
+
label,
|
|
25
|
+
isValid,
|
|
26
|
+
successColor,
|
|
27
|
+
pendingColor,
|
|
28
|
+
}) => {
|
|
29
|
+
const color = isValid ? successColor : pendingColor;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View style={styles.requirement}>
|
|
33
|
+
<View style={[styles.dot, { backgroundColor: color }]} />
|
|
34
|
+
<Text style={[styles.label, { color }]}>{label}</Text>
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const PasswordStrengthIndicator: React.FC<
|
|
40
|
+
PasswordStrengthIndicatorProps
|
|
41
|
+
> = ({ requirements, showLabels = true }) => {
|
|
42
|
+
const tokens = useAppDesignTokens();
|
|
43
|
+
const successColor = tokens.colors.success;
|
|
44
|
+
const pendingColor = tokens.colors.textTertiary;
|
|
45
|
+
|
|
46
|
+
const items = [
|
|
47
|
+
{ key: "minLength", label: "8+", isValid: requirements.hasMinLength },
|
|
48
|
+
{ key: "uppercase", label: "A-Z", isValid: requirements.hasUppercase },
|
|
49
|
+
{ key: "lowercase", label: "a-z", isValid: requirements.hasLowercase },
|
|
50
|
+
{ key: "number", label: "0-9", isValid: requirements.hasNumber },
|
|
51
|
+
{ key: "special", label: "!@#", isValid: requirements.hasSpecialChar },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (!showLabels) {
|
|
55
|
+
return (
|
|
56
|
+
<View style={styles.dotsOnly}>
|
|
57
|
+
{items.map((item) => (
|
|
58
|
+
<View
|
|
59
|
+
key={item.key}
|
|
60
|
+
style={[
|
|
61
|
+
styles.dotOnly,
|
|
62
|
+
{
|
|
63
|
+
backgroundColor: item.isValid ? successColor : pendingColor,
|
|
64
|
+
},
|
|
65
|
+
]}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<View style={styles.container}>
|
|
74
|
+
{items.map((item) => (
|
|
75
|
+
<RequirementDot
|
|
76
|
+
key={item.key}
|
|
77
|
+
label={item.label}
|
|
78
|
+
isValid={item.isValid}
|
|
79
|
+
successColor={successColor}
|
|
80
|
+
pendingColor={pendingColor}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</View>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const styles = StyleSheet.create({
|
|
88
|
+
container: {
|
|
89
|
+
flexDirection: "row",
|
|
90
|
+
flexWrap: "wrap",
|
|
91
|
+
gap: 12,
|
|
92
|
+
marginTop: 8,
|
|
93
|
+
},
|
|
94
|
+
dotsOnly: {
|
|
95
|
+
flexDirection: "row",
|
|
96
|
+
gap: 6,
|
|
97
|
+
marginTop: 8,
|
|
98
|
+
},
|
|
99
|
+
requirement: {
|
|
100
|
+
flexDirection: "row",
|
|
101
|
+
alignItems: "center",
|
|
102
|
+
gap: 4,
|
|
103
|
+
},
|
|
104
|
+
dot: {
|
|
105
|
+
width: 6,
|
|
106
|
+
height: 6,
|
|
107
|
+
borderRadius: 3,
|
|
108
|
+
},
|
|
109
|
+
dotOnly: {
|
|
110
|
+
width: 8,
|
|
111
|
+
height: 8,
|
|
112
|
+
borderRadius: 4,
|
|
113
|
+
},
|
|
114
|
+
label: {
|
|
115
|
+
fontSize: 11,
|
|
116
|
+
fontWeight: "500",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
@@ -11,6 +11,8 @@ import { useRegisterForm } from "../hooks/useRegisterForm";
|
|
|
11
11
|
import { AuthErrorDisplay } from "./AuthErrorDisplay";
|
|
12
12
|
import { AuthLink } from "./AuthLink";
|
|
13
13
|
import { AuthLegalLinks } from "./AuthLegalLinks";
|
|
14
|
+
import { PasswordStrengthIndicator } from "./PasswordStrengthIndicator";
|
|
15
|
+
import { PasswordMatchIndicator } from "./PasswordMatchIndicator";
|
|
14
16
|
|
|
15
17
|
interface RegisterFormProps {
|
|
16
18
|
onNavigateToLogin: () => void;
|
|
@@ -35,6 +37,8 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|
|
35
37
|
confirmPassword,
|
|
36
38
|
fieldErrors,
|
|
37
39
|
loading,
|
|
40
|
+
passwordRequirements,
|
|
41
|
+
passwordsMatch,
|
|
38
42
|
handleDisplayNameChange,
|
|
39
43
|
handleEmailChange,
|
|
40
44
|
handlePasswordChange,
|
|
@@ -86,6 +90,9 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|
|
86
90
|
state={fieldErrors.password ? "error" : "default"}
|
|
87
91
|
helperText={fieldErrors.password || undefined}
|
|
88
92
|
/>
|
|
93
|
+
{password.length > 0 && (
|
|
94
|
+
<PasswordStrengthIndicator requirements={passwordRequirements} />
|
|
95
|
+
)}
|
|
89
96
|
</View>
|
|
90
97
|
|
|
91
98
|
<View style={styles.inputContainer}>
|
|
@@ -102,6 +109,9 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
|
|
102
109
|
state={fieldErrors.confirmPassword ? "error" : "default"}
|
|
103
110
|
helperText={fieldErrors.confirmPassword || undefined}
|
|
104
111
|
/>
|
|
112
|
+
{confirmPassword.length > 0 && (
|
|
113
|
+
<PasswordMatchIndicator isMatch={passwordsMatch} />
|
|
114
|
+
)}
|
|
105
115
|
</View>
|
|
106
116
|
|
|
107
117
|
<AuthErrorDisplay error={displayError} />
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Single Responsibility: Handle register form logic
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback } from "react";
|
|
6
|
+
import { useState, useCallback, useMemo } from "react";
|
|
7
7
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
8
8
|
import { batchValidate } from "@umituz/react-native-validation";
|
|
9
9
|
import {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { DEFAULT_PASSWORD_CONFIG } from "../../domain/value-objects/AuthConfig";
|
|
15
15
|
import { useAuth } from "./useAuth";
|
|
16
16
|
import { getAuthErrorLocalizationKey } from "../utils/getAuthErrorMessage";
|
|
17
|
+
import type { PasswordRequirements } from "../../infrastructure/utils/AuthValidation";
|
|
17
18
|
|
|
18
19
|
export interface UseRegisterFormResult {
|
|
19
20
|
displayName: string;
|
|
@@ -28,6 +29,8 @@ export interface UseRegisterFormResult {
|
|
|
28
29
|
};
|
|
29
30
|
localError: string | null;
|
|
30
31
|
loading: boolean;
|
|
32
|
+
passwordRequirements: PasswordRequirements;
|
|
33
|
+
passwordsMatch: boolean;
|
|
31
34
|
handleDisplayNameChange: (text: string) => void;
|
|
32
35
|
handleEmailChange: (text: string) => void;
|
|
33
36
|
handlePasswordChange: (text: string) => void;
|
|
@@ -55,6 +58,24 @@ export function useRegisterForm(): UseRegisterFormResult {
|
|
|
55
58
|
confirmPassword?: string;
|
|
56
59
|
}>({});
|
|
57
60
|
|
|
61
|
+
const passwordRequirements = useMemo((): PasswordRequirements => {
|
|
62
|
+
if (!password) {
|
|
63
|
+
return {
|
|
64
|
+
hasMinLength: false,
|
|
65
|
+
hasUppercase: false,
|
|
66
|
+
hasLowercase: false,
|
|
67
|
+
hasNumber: false,
|
|
68
|
+
hasSpecialChar: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const result = validatePasswordForRegister(password, DEFAULT_PASSWORD_CONFIG);
|
|
72
|
+
return result.requirements;
|
|
73
|
+
}, [password]);
|
|
74
|
+
|
|
75
|
+
const passwordsMatch = useMemo(() => {
|
|
76
|
+
return password.length > 0 && password === confirmPassword;
|
|
77
|
+
}, [password, confirmPassword]);
|
|
78
|
+
|
|
58
79
|
const handleDisplayNameChange = useCallback((text: string) => {
|
|
59
80
|
setDisplayName(text);
|
|
60
81
|
setFieldErrors((prev) => {
|
|
@@ -153,6 +174,8 @@ export function useRegisterForm(): UseRegisterFormResult {
|
|
|
153
174
|
fieldErrors,
|
|
154
175
|
localError,
|
|
155
176
|
loading,
|
|
177
|
+
passwordRequirements,
|
|
178
|
+
passwordsMatch,
|
|
156
179
|
handleDisplayNameChange,
|
|
157
180
|
handleEmailChange,
|
|
158
181
|
handlePasswordChange,
|