@umituz/react-native-auth 1.1.2 → 1.2.1

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": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "Firebase Authentication wrapper for React Native apps - Secure, type-safe, and production-ready",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -33,7 +33,16 @@
33
33
  "peerDependencies": {
34
34
  "firebase": ">=11.0.0",
35
35
  "react": ">=18.2.0",
36
- "react-native": ">=0.74.0"
36
+ "react-native": ">=0.74.0",
37
+ "@react-navigation/stack": "^6.0.0",
38
+ "@react-navigation/native": "^6.0.0",
39
+ "@umituz/react-native-design-system": "*",
40
+ "@umituz/react-native-theme": "*",
41
+ "@umituz/react-native-localization": "*",
42
+ "@umituz/react-native-validation": "*",
43
+ "@umituz/react-native-storage": "*",
44
+ "expo-linear-gradient": "^13.0.0",
45
+ "react-native-safe-area-context": "^4.0.0"
37
46
  },
38
47
  "devDependencies": {
39
48
  "firebase": "^11.10.0",
@@ -9,7 +9,6 @@ export interface SignUpParams {
9
9
  email: string;
10
10
  password: string;
11
11
  displayName?: string;
12
- username?: string;
13
12
  }
14
13
 
15
14
  export interface SignInParams {
@@ -83,11 +83,3 @@ export class AuthInvalidEmailError extends AuthError {
83
83
  }
84
84
  }
85
85
 
86
- export class AuthInvalidCredentialError extends AuthError {
87
- constructor(message: string = "Invalid email or password") {
88
- super(message, "AUTH_INVALID_CREDENTIAL");
89
- this.name = "AuthInvalidCredentialError";
90
- Object.setPrototypeOf(this, AuthInvalidCredentialError.prototype);
91
- }
92
- }
93
-
@@ -15,15 +15,11 @@ export interface AuthConfig {
15
15
  /** Require special characters in password */
16
16
  requireSpecialChars?: boolean;
17
17
  /** Callback for user profile creation after signup */
18
- onUserCreated?: (user: any, context?: { username?: string; displayName?: string }) => Promise<void> | void;
18
+ onUserCreated?: (user: any) => Promise<void> | void;
19
19
  /** Callback for user profile update */
20
20
  onUserUpdated?: (user: any) => Promise<void> | void;
21
- /** Callback after successful sign in */
22
- onSignIn?: (user: any) => Promise<void> | void;
23
21
  /** Callback for sign out cleanup */
24
22
  onSignOut?: () => Promise<void> | void;
25
- /** Callback when guest mode is enabled */
26
- onGuestModeEnabled?: () => Promise<void> | void;
27
23
  }
28
24
 
29
25
  export const DEFAULT_AUTH_CONFIG: Required<Omit<AuthConfig, 'onUserCreated' | 'onUserUpdated' | 'onSignOut'>> = {
package/src/index.ts CHANGED
@@ -31,7 +31,6 @@ export {
31
31
  AuthEmailAlreadyInUseError,
32
32
  AuthWeakPasswordError,
33
33
  AuthInvalidEmailError,
34
- AuthInvalidCredentialError,
35
34
  } from './domain/errors/AuthError';
36
35
 
37
36
  export type { AuthConfig } from './domain/value-objects/AuthConfig';
@@ -61,3 +60,12 @@ export {
61
60
  export { useAuth } from './presentation/hooks/useAuth';
62
61
  export type { UseAuthResult } from './presentation/hooks/useAuth';
63
62
 
63
+ // =============================================================================
64
+ // PRESENTATION LAYER - Screens & Navigation
65
+ // =============================================================================
66
+
67
+ export { LoginScreen } from './presentation/screens/LoginScreen';
68
+ export { RegisterScreen } from './presentation/screens/RegisterScreen';
69
+ export { AuthNavigator } from './presentation/navigation/AuthNavigator';
70
+ export type { AuthStackParamList } from './presentation/navigation/AuthNavigator';
71
+
@@ -14,7 +14,6 @@ import {
14
14
  } from "firebase/auth";
15
15
  import type { IAuthService, SignUpParams, SignInParams } from "../../application/ports/IAuthService";
16
16
  import {
17
- AuthError,
18
17
  AuthInitializationError,
19
18
  AuthConfigurationError,
20
19
  AuthValidationError,
@@ -24,7 +23,6 @@ import {
24
23
  AuthWrongPasswordError,
25
24
  AuthUserNotFoundError,
26
25
  AuthNetworkError,
27
- AuthInvalidCredentialError,
28
26
  } from "../../domain/errors/AuthError";
29
27
  import type { AuthConfig } from "../../domain/value-objects/AuthConfig";
30
28
  import { DEFAULT_AUTH_CONFIG } from "../../domain/value-objects/AuthConfig";
@@ -117,10 +115,6 @@ function mapFirebaseAuthError(error: any): Error {
117
115
  if (code === "auth/too-many-requests") {
118
116
  return new AuthError("Too many requests. Please try again later.", "AUTH_TOO_MANY_REQUESTS");
119
117
  }
120
- // Firebase v9+ uses auth/invalid-credential for both wrong email and wrong password
121
- if (code === "auth/invalid-credential") {
122
- return new AuthInvalidCredentialError();
123
- }
124
118
 
125
119
  return new AuthError(message, code);
126
120
  }
@@ -207,10 +201,7 @@ export class AuthService implements IAuthService {
207
201
  // Call user created callback if provided
208
202
  if (this.config.onUserCreated) {
209
203
  try {
210
- await this.config.onUserCreated(userCredential.user, {
211
- username: params.username,
212
- displayName: params.displayName,
213
- });
204
+ await this.config.onUserCreated(userCredential.user);
214
205
  } catch (callbackError) {
215
206
  // Don't fail signup if callback fails
216
207
  }
@@ -249,16 +240,6 @@ export class AuthService implements IAuthService {
249
240
  );
250
241
 
251
242
  this.isGuestMode = false;
252
-
253
- // Call sign in callback if provided
254
- if (this.config.onSignIn) {
255
- try {
256
- await this.config.onSignIn(userCredential.user);
257
- } catch (callbackError) {
258
- // Don't fail signin if callback fails
259
- }
260
- }
261
-
262
243
  return userCredential.user;
263
244
  } catch (error: any) {
264
245
  throw mapFirebaseAuthError(error);
@@ -309,15 +290,6 @@ export class AuthService implements IAuthService {
309
290
  }
310
291
 
311
292
  this.isGuestMode = true;
312
-
313
- // Call guest mode enabled callback if provided
314
- if (this.config.onGuestModeEnabled) {
315
- try {
316
- await this.config.onGuestModeEnabled();
317
- } catch (callbackError) {
318
- // Don't fail guest mode if callback fails
319
- }
320
- }
321
293
  }
322
294
 
323
295
  /**
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Auth Container Component
3
+ * Main container for auth screens with gradient and scroll
4
+ */
5
+
6
+ import React from "react";
7
+ import {
8
+ View,
9
+ StyleSheet,
10
+ ScrollView,
11
+ KeyboardAvoidingView,
12
+ Platform,
13
+ } from "react-native";
14
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
15
+ import { AuthGradientBackground } from "./AuthGradientBackground";
16
+
17
+ interface AuthContainerProps {
18
+ children: React.ReactNode;
19
+ }
20
+
21
+ export const AuthContainer: React.FC<AuthContainerProps> = ({ children }) => {
22
+ const insets = useSafeAreaInsets();
23
+
24
+ return (
25
+ <KeyboardAvoidingView
26
+ style={styles.container}
27
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
28
+ keyboardVerticalOffset={Platform.OS === "ios" ? 0 : 20}
29
+ >
30
+ <AuthGradientBackground />
31
+ <ScrollView
32
+ contentContainerStyle={[
33
+ styles.scrollContent,
34
+ { paddingTop: insets.top + 40, paddingBottom: insets.bottom + 40 },
35
+ ]}
36
+ keyboardShouldPersistTaps="handled"
37
+ showsVerticalScrollIndicator={false}
38
+ >
39
+ <View style={styles.content}>{children}</View>
40
+ </ScrollView>
41
+ </KeyboardAvoidingView>
42
+ );
43
+ };
44
+
45
+ const styles = StyleSheet.create({
46
+ container: {
47
+ flex: 1,
48
+ },
49
+ scrollContent: {
50
+ flexGrow: 1,
51
+ paddingHorizontal: 20,
52
+ },
53
+ content: {
54
+ flex: 1,
55
+ justifyContent: "center",
56
+ maxWidth: 440,
57
+ alignSelf: "center",
58
+ width: "100%",
59
+ },
60
+ });
61
+
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Auth Divider Component
3
+ * Divider with "OR" text for auth screens
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, Text, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-theme";
9
+ import { useLocalization } from "@umituz/react-native-localization";
10
+
11
+ export const AuthDivider: React.FC = () => {
12
+ const tokens = useAppDesignTokens();
13
+ const { t } = useLocalization();
14
+
15
+ return (
16
+ <View style={styles.divider}>
17
+ <View
18
+ style={[
19
+ styles.dividerLine,
20
+ { backgroundColor: tokens.colors.borderLight || "#E5E5E5" },
21
+ ]}
22
+ />
23
+ <Text
24
+ style={[
25
+ styles.dividerText,
26
+ { color: tokens.colors.textSecondary || "#999999" },
27
+ ]}
28
+ >
29
+ {t("general.or")}
30
+ </Text>
31
+ <View
32
+ style={[
33
+ styles.dividerLine,
34
+ { backgroundColor: tokens.colors.borderLight || "#E5E5E5" },
35
+ ]}
36
+ />
37
+ </View>
38
+ );
39
+ };
40
+
41
+ const styles = StyleSheet.create({
42
+ divider: {
43
+ flexDirection: "row",
44
+ alignItems: "center",
45
+ marginVertical: 20,
46
+ },
47
+ dividerLine: {
48
+ flex: 1,
49
+ height: 1,
50
+ },
51
+ dividerText: {
52
+ marginHorizontal: 16,
53
+ fontSize: 13,
54
+ fontWeight: "500",
55
+ textTransform: "uppercase",
56
+ letterSpacing: 0.5,
57
+ },
58
+ });
59
+
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Auth Error Display Component
3
+ * Displays authentication errors
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, Text, StyleSheet } from "react-native";
8
+
9
+ interface AuthErrorDisplayProps {
10
+ error: string | null;
11
+ }
12
+
13
+ export const AuthErrorDisplay: React.FC<AuthErrorDisplayProps> = ({
14
+ error,
15
+ }) => {
16
+ if (!error) {
17
+ return null;
18
+ }
19
+
20
+ return (
21
+ <View style={styles.errorContainer}>
22
+ <Text style={styles.errorText}>{error}</Text>
23
+ </View>
24
+ );
25
+ };
26
+
27
+ const styles = StyleSheet.create({
28
+ errorContainer: {
29
+ marginBottom: 16,
30
+ padding: 14,
31
+ borderRadius: 12,
32
+ backgroundColor: "rgba(255, 59, 48, 0.1)",
33
+ borderWidth: 1,
34
+ borderColor: "rgba(255, 59, 48, 0.2)",
35
+ },
36
+ errorText: {
37
+ color: "#FF3B30",
38
+ fontSize: 14,
39
+ textAlign: "center",
40
+ fontWeight: "500",
41
+ },
42
+ });
43
+
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Auth Form Card Component
3
+ * Reusable card container for auth forms
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-theme";
9
+
10
+ interface AuthFormCardProps {
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ export const AuthFormCard: React.FC<AuthFormCardProps> = ({ children }) => {
15
+ const tokens = useAppDesignTokens();
16
+
17
+ return (
18
+ <View
19
+ style={[
20
+ styles.formCard,
21
+ { backgroundColor: tokens.colors.surface || "#FFFFFF" },
22
+ ]}
23
+ >
24
+ <View style={styles.form}>{children}</View>
25
+ </View>
26
+ );
27
+ };
28
+
29
+ const styles = StyleSheet.create({
30
+ formCard: {
31
+ borderRadius: 24,
32
+ padding: 24,
33
+ },
34
+ form: {
35
+ width: "100%",
36
+ },
37
+ });
38
+
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Auth Gradient Background Component
3
+ * Gradient background for auth screens
4
+ */
5
+
6
+ import React from "react";
7
+ import { StyleSheet } from "react-native";
8
+ import { LinearGradient } from "expo-linear-gradient";
9
+ import { useAppDesignTokens } from "@umituz/react-native-theme";
10
+
11
+ export const AuthGradientBackground: React.FC = () => {
12
+ const tokens = useAppDesignTokens();
13
+
14
+ return (
15
+ <LinearGradient
16
+ colors={[tokens.colors.primary, tokens.colors.secondary]}
17
+ start={{ x: 0, y: 0 }}
18
+ end={{ x: 1, y: 1 }}
19
+ style={StyleSheet.absoluteFill}
20
+ />
21
+ );
22
+ };
23
+
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Auth Header Component
3
+ * Reusable header for auth screens
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, Text, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens } from "@umituz/react-native-theme";
9
+ import { useLocalization } from "@umituz/react-native-localization";
10
+
11
+ interface AuthHeaderProps {
12
+ title: string;
13
+ subtitle?: string;
14
+ }
15
+
16
+ export const AuthHeader: React.FC<AuthHeaderProps> = ({ title, subtitle }) => {
17
+ const tokens = useAppDesignTokens();
18
+ const { t } = useLocalization();
19
+
20
+ return (
21
+ <View style={styles.header}>
22
+ <Text
23
+ style={[
24
+ styles.title,
25
+ { color: tokens.colors.onPrimary || "#FFFFFF" },
26
+ ]}
27
+ >
28
+ {title}
29
+ </Text>
30
+ {(subtitle || t("auth.subtitle")) && (
31
+ <Text
32
+ style={[
33
+ styles.subtitle,
34
+ {
35
+ color:
36
+ tokens.colors.textInverse || "rgba(255, 255, 255, 0.9)",
37
+ },
38
+ ]}
39
+ >
40
+ {subtitle || t("auth.subtitle")}
41
+ </Text>
42
+ )}
43
+ </View>
44
+ );
45
+ };
46
+
47
+ const styles = StyleSheet.create({
48
+ header: {
49
+ marginBottom: 28,
50
+ alignItems: "center",
51
+ paddingHorizontal: 20,
52
+ },
53
+ title: {
54
+ fontSize: 36,
55
+ fontWeight: "700",
56
+ marginBottom: 8,
57
+ textAlign: "center",
58
+ letterSpacing: -0.5,
59
+ },
60
+ subtitle: {
61
+ fontSize: 16,
62
+ textAlign: "center",
63
+ lineHeight: 22,
64
+ fontWeight: "400",
65
+ marginTop: 4,
66
+ },
67
+ });
68
+
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Auth Link Component
3
+ * Link text with button for navigation between auth screens
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, Text, StyleSheet } from "react-native";
8
+ import { AtomicButton } from "@umituz/react-native-design-system";
9
+ import { useAppDesignTokens } from "@umituz/react-native-theme";
10
+
11
+ interface AuthLinkProps {
12
+ text: string;
13
+ linkText: string;
14
+ onPress: () => void;
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export const AuthLink: React.FC<AuthLinkProps> = ({
19
+ text,
20
+ linkText,
21
+ onPress,
22
+ disabled = false,
23
+ }) => {
24
+ const tokens = useAppDesignTokens();
25
+
26
+ return (
27
+ <View style={styles.container}>
28
+ <Text
29
+ style={[
30
+ styles.text,
31
+ { color: tokens.colors.textSecondary || "#666666" },
32
+ ]}
33
+ >
34
+ {text}{" "}
35
+ </Text>
36
+ <AtomicButton
37
+ variant="text"
38
+ onPress={onPress}
39
+ disabled={disabled}
40
+ style={styles.button}
41
+ >
42
+ {linkText}
43
+ </AtomicButton>
44
+ </View>
45
+ );
46
+ };
47
+
48
+ const styles = StyleSheet.create({
49
+ container: {
50
+ flexDirection: "row",
51
+ justifyContent: "center",
52
+ alignItems: "center",
53
+ marginTop: 8,
54
+ paddingTop: 8,
55
+ },
56
+ text: {
57
+ fontSize: 15,
58
+ fontWeight: "400",
59
+ },
60
+ button: {
61
+ paddingHorizontal: 4,
62
+ },
63
+ });
64
+
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Login Form Component
3
+ * Form fields and actions for login
4
+ */
5
+
6
+ import React, { useState } from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { AtomicInput, AtomicButton } from "@umituz/react-native-design-system";
9
+ import { useLocalization } from "@umituz/react-native-localization";
10
+ import { useAuth } from "../hooks/useAuth";
11
+ import { AuthErrorDisplay } from "./AuthErrorDisplay";
12
+ import { AuthDivider } from "./AuthDivider";
13
+ import { AuthLink } from "./AuthLink";
14
+
15
+ interface LoginFormProps {
16
+ onNavigateToRegister: () => void;
17
+ }
18
+
19
+ export const LoginForm: React.FC<LoginFormProps> = ({
20
+ onNavigateToRegister,
21
+ }) => {
22
+ const { t } = useLocalization();
23
+ const { signIn, loading, error, continueAsGuest } = useAuth();
24
+
25
+ const [email, setEmail] = useState("");
26
+ const [password, setPassword] = useState("");
27
+ const [emailError, setEmailError] = useState<string | null>(null);
28
+ const [passwordError, setPasswordError] = useState<string | null>(null);
29
+ const [localError, setLocalError] = useState<string | null>(null);
30
+
31
+ const validateEmail = (emailValue: string): boolean => {
32
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
33
+ return emailRegex.test(emailValue);
34
+ };
35
+
36
+ const handleEmailChange = (text: string) => {
37
+ setEmail(text);
38
+ if (emailError) setEmailError(null);
39
+ if (localError) setLocalError(null);
40
+ };
41
+
42
+ const handlePasswordChange = (text: string) => {
43
+ setPassword(text);
44
+ if (passwordError) setPasswordError(null);
45
+ if (localError) setLocalError(null);
46
+ };
47
+
48
+ const handleSignIn = async () => {
49
+ setEmailError(null);
50
+ setPasswordError(null);
51
+ setLocalError(null);
52
+
53
+ let hasError = false;
54
+
55
+ if (!email.trim()) {
56
+ setEmailError(t("auth.errors.invalidEmail"));
57
+ hasError = true;
58
+ } else if (!validateEmail(email.trim())) {
59
+ setEmailError(t("auth.errors.invalidEmail"));
60
+ hasError = true;
61
+ }
62
+
63
+ if (!password.trim()) {
64
+ setPasswordError(t("auth.errors.weakPassword"));
65
+ hasError = true;
66
+ } else if (password.length < 6) {
67
+ setPasswordError(t("auth.errors.weakPassword"));
68
+ hasError = true;
69
+ }
70
+
71
+ if (hasError) return;
72
+
73
+ try {
74
+ await signIn(email.trim(), password);
75
+ } catch (err: any) {
76
+ const errorMessage = err.message || t("auth.errors.unknownError");
77
+ setLocalError(errorMessage);
78
+ }
79
+ };
80
+
81
+ const handleContinueAsGuest = async () => {
82
+ try {
83
+ await continueAsGuest();
84
+ } catch (err) {
85
+ // Error handling is done in the hook
86
+ }
87
+ };
88
+
89
+ const displayError = localError || error;
90
+
91
+ return (
92
+ <>
93
+ <View style={styles.inputContainer}>
94
+ <AtomicInput
95
+ label={t("auth.email")}
96
+ value={email}
97
+ onChangeText={handleEmailChange}
98
+ placeholder={t("auth.emailPlaceholder")}
99
+ keyboardType="email-address"
100
+ autoCapitalize="none"
101
+ editable={!loading}
102
+ state={emailError ? "error" : "default"}
103
+ helperText={emailError || undefined}
104
+ />
105
+ </View>
106
+
107
+ <View style={styles.inputContainer}>
108
+ <AtomicInput
109
+ label={t("auth.password")}
110
+ value={password}
111
+ onChangeText={handlePasswordChange}
112
+ placeholder={t("auth.passwordPlaceholder")}
113
+ secureTextEntry
114
+ autoCapitalize="none"
115
+ editable={!loading}
116
+ state={passwordError ? "error" : "default"}
117
+ helperText={passwordError || undefined}
118
+ />
119
+ </View>
120
+
121
+ <AuthErrorDisplay error={displayError} />
122
+
123
+ <View style={styles.buttonContainer}>
124
+ <AtomicButton
125
+ variant="primary"
126
+ onPress={handleSignIn}
127
+ disabled={loading || !email.trim() || !password.trim()}
128
+ fullWidth
129
+ loading={loading}
130
+ style={styles.signInButton}
131
+ >
132
+ {t("auth.signIn")}
133
+ </AtomicButton>
134
+ </View>
135
+
136
+ <AuthDivider />
137
+
138
+ <View style={styles.buttonContainer}>
139
+ <AtomicButton
140
+ variant="outline"
141
+ onPress={handleContinueAsGuest}
142
+ disabled={loading}
143
+ fullWidth
144
+ style={styles.guestButton}
145
+ >
146
+ {t("auth.continueAsGuest")}
147
+ </AtomicButton>
148
+ </View>
149
+
150
+ <AuthLink
151
+ text={t("auth.dontHaveAccount")}
152
+ linkText={t("auth.createAccount")}
153
+ onPress={onNavigateToRegister}
154
+ disabled={loading}
155
+ />
156
+ </>
157
+ );
158
+ };
159
+
160
+ const styles = StyleSheet.create({
161
+ inputContainer: {
162
+ marginBottom: 20,
163
+ },
164
+ buttonContainer: {
165
+ marginBottom: 16,
166
+ },
167
+ signInButton: {
168
+ minHeight: 52,
169
+ },
170
+ guestButton: {
171
+ minHeight: 52,
172
+ },
173
+ });
174
+
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Register Form Component
3
+ * Form fields and actions for registration
4
+ */
5
+
6
+ import React, { useState } from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { AtomicInput, AtomicButton } from "@umituz/react-native-design-system";
9
+ import { useLocalization } from "@umituz/react-native-localization";
10
+ import {
11
+ validateEmail,
12
+ validatePassword,
13
+ validatePasswordConfirmation,
14
+ batchValidate,
15
+ } from "@umituz/react-native-validation";
16
+ import { useAuth } from "../hooks/useAuth";
17
+ import { AuthErrorDisplay } from "./AuthErrorDisplay";
18
+ import { AuthLink } from "./AuthLink";
19
+
20
+ interface RegisterFormProps {
21
+ onNavigateToLogin: () => void;
22
+ }
23
+
24
+ export const RegisterForm: React.FC<RegisterFormProps> = ({
25
+ onNavigateToLogin,
26
+ }) => {
27
+ const { t } = useLocalization();
28
+ const { signUp, loading, error } = useAuth();
29
+
30
+ const [displayName, setDisplayName] = useState("");
31
+ const [email, setEmail] = useState("");
32
+ const [password, setPassword] = useState("");
33
+ const [confirmPassword, setConfirmPassword] = useState("");
34
+ const [localError, setLocalError] = useState<string | null>(null);
35
+ const [fieldErrors, setFieldErrors] = useState<{
36
+ displayName?: string;
37
+ email?: string;
38
+ password?: string;
39
+ confirmPassword?: string;
40
+ }>({});
41
+
42
+ const handleDisplayNameChange = (text: string) => {
43
+ setDisplayName(text);
44
+ if (fieldErrors.displayName) {
45
+ setFieldErrors({ ...fieldErrors, displayName: undefined });
46
+ }
47
+ if (localError) setLocalError(null);
48
+ };
49
+
50
+ const handleEmailChange = (text: string) => {
51
+ setEmail(text);
52
+ if (fieldErrors.email) {
53
+ setFieldErrors({ ...fieldErrors, email: undefined });
54
+ }
55
+ if (localError) setLocalError(null);
56
+ };
57
+
58
+ const handlePasswordChange = (text: string) => {
59
+ setPassword(text);
60
+ if (fieldErrors.password) {
61
+ setFieldErrors({ ...fieldErrors, password: undefined });
62
+ }
63
+ if (fieldErrors.confirmPassword) {
64
+ setFieldErrors({ ...fieldErrors, confirmPassword: undefined });
65
+ }
66
+ if (localError) setLocalError(null);
67
+ };
68
+
69
+ const handleConfirmPasswordChange = (text: string) => {
70
+ setConfirmPassword(text);
71
+ if (fieldErrors.confirmPassword) {
72
+ setFieldErrors({ ...fieldErrors, confirmPassword: undefined });
73
+ }
74
+ if (localError) setLocalError(null);
75
+ };
76
+
77
+ const handleSignUp = async () => {
78
+ setLocalError(null);
79
+ setFieldErrors({});
80
+
81
+ const validationResult = batchValidate([
82
+ {
83
+ field: "email",
84
+ validator: () => validateEmail(email.trim()),
85
+ },
86
+ {
87
+ field: "password",
88
+ validator: () =>
89
+ validatePassword(password, {
90
+ minLength: 6,
91
+ requireUppercase: false,
92
+ requireLowercase: false,
93
+ requireNumber: false,
94
+ }),
95
+ },
96
+ {
97
+ field: "confirmPassword",
98
+ validator: () =>
99
+ validatePasswordConfirmation(password, confirmPassword),
100
+ },
101
+ ]);
102
+
103
+ if (!validationResult.isValid) {
104
+ setFieldErrors(validationResult.errors);
105
+ return;
106
+ }
107
+
108
+ try {
109
+ await signUp(
110
+ email.trim(),
111
+ password,
112
+ displayName.trim() || undefined,
113
+ );
114
+ } catch (err: any) {
115
+ const errorMessage = err.message || t("auth.errors.unknownError");
116
+ setLocalError(errorMessage);
117
+ }
118
+ };
119
+
120
+ const displayError = localError || error;
121
+
122
+ return (
123
+ <>
124
+ <View style={styles.inputContainer}>
125
+ <AtomicInput
126
+ label={t("auth.displayName") || "Full Name"}
127
+ value={displayName}
128
+ onChangeText={handleDisplayNameChange}
129
+ placeholder={
130
+ t("auth.displayNamePlaceholder") || "Enter your full name"
131
+ }
132
+ autoCapitalize="words"
133
+ editable={!loading}
134
+ state={fieldErrors.displayName ? "error" : "default"}
135
+ helperText={fieldErrors.displayName || undefined}
136
+ />
137
+ </View>
138
+
139
+ <View style={styles.inputContainer}>
140
+ <AtomicInput
141
+ label={t("auth.email")}
142
+ value={email}
143
+ onChangeText={handleEmailChange}
144
+ placeholder={t("auth.emailPlaceholder")}
145
+ keyboardType="email-address"
146
+ autoCapitalize="none"
147
+ editable={!loading}
148
+ state={fieldErrors.email ? "error" : "default"}
149
+ helperText={fieldErrors.email || undefined}
150
+ />
151
+ </View>
152
+
153
+ <View style={styles.inputContainer}>
154
+ <AtomicInput
155
+ label={t("auth.password")}
156
+ value={password}
157
+ onChangeText={handlePasswordChange}
158
+ placeholder={t("auth.passwordPlaceholder")}
159
+ secureTextEntry
160
+ autoCapitalize="none"
161
+ editable={!loading}
162
+ state={fieldErrors.password ? "error" : "default"}
163
+ helperText={fieldErrors.password || undefined}
164
+ />
165
+ </View>
166
+
167
+ <View style={styles.inputContainer}>
168
+ <AtomicInput
169
+ label={t("auth.confirmPassword") || "Confirm Password"}
170
+ value={confirmPassword}
171
+ onChangeText={handleConfirmPasswordChange}
172
+ placeholder={
173
+ t("auth.confirmPasswordPlaceholder") || "Confirm your password"
174
+ }
175
+ secureTextEntry
176
+ autoCapitalize="none"
177
+ editable={!loading}
178
+ state={fieldErrors.confirmPassword ? "error" : "default"}
179
+ helperText={fieldErrors.confirmPassword || undefined}
180
+ />
181
+ </View>
182
+
183
+ <AuthErrorDisplay error={displayError} />
184
+
185
+ <View style={styles.buttonContainer}>
186
+ <AtomicButton
187
+ variant="primary"
188
+ onPress={handleSignUp}
189
+ disabled={
190
+ loading ||
191
+ !email.trim() ||
192
+ !password.trim() ||
193
+ !confirmPassword.trim()
194
+ }
195
+ fullWidth
196
+ loading={loading}
197
+ style={styles.signUpButton}
198
+ >
199
+ {t("auth.signUp")}
200
+ </AtomicButton>
201
+ </View>
202
+
203
+ <AuthLink
204
+ text={t("auth.alreadyHaveAccount")}
205
+ linkText={t("auth.signIn")}
206
+ onPress={onNavigateToLogin}
207
+ disabled={loading}
208
+ />
209
+ </>
210
+ );
211
+ };
212
+
213
+ const styles = StyleSheet.create({
214
+ inputContainer: {
215
+ marginBottom: 20,
216
+ },
217
+ buttonContainer: {
218
+ marginBottom: 16,
219
+ marginTop: 8,
220
+ },
221
+ signUpButton: {
222
+ minHeight: 52,
223
+ },
224
+ });
225
+
@@ -16,6 +16,8 @@ export interface UseAuthResult {
16
16
  isGuest: boolean;
17
17
  /** Whether user is authenticated */
18
18
  isAuthenticated: boolean;
19
+ /** Current error message */
20
+ error: string | null;
19
21
  /** Sign up function */
20
22
  signUp: (email: string, password: string, displayName?: string) => Promise<void>;
21
23
  /** Sign in function */
@@ -38,6 +40,7 @@ export function useAuth(): UseAuthResult {
38
40
  const [user, setUser] = useState<User | null>(null);
39
41
  const [loading, setLoading] = useState(true);
40
42
  const [isGuest, setIsGuest] = useState(false);
43
+ const [error, setError] = useState<string | null>(null);
41
44
 
42
45
  useEffect(() => {
43
46
  const service = getAuthService();
@@ -77,19 +80,37 @@ export function useAuth(): UseAuthResult {
77
80
  const signUp = useCallback(async (email: string, password: string, displayName?: string) => {
78
81
  const service = getAuthService();
79
82
  if (!service) {
80
- throw new Error("Auth service is not initialized");
83
+ const err = "Auth service is not initialized";
84
+ setError(err);
85
+ throw new Error(err);
86
+ }
87
+ try {
88
+ setError(null);
89
+ await service.signUp({ email, password, displayName });
90
+ // State will be updated via onAuthStateChange
91
+ } catch (err: any) {
92
+ const errorMessage = err.message || "Sign up failed";
93
+ setError(errorMessage);
94
+ throw err;
81
95
  }
82
- await service.signUp({ email, password, displayName });
83
- // State will be updated via onAuthStateChange
84
96
  }, []);
85
97
 
86
98
  const signIn = useCallback(async (email: string, password: string) => {
87
99
  const service = getAuthService();
88
100
  if (!service) {
89
- throw new Error("Auth service is not initialized");
101
+ const err = "Auth service is not initialized";
102
+ setError(err);
103
+ throw new Error(err);
104
+ }
105
+ try {
106
+ setError(null);
107
+ await service.signIn({ email, password });
108
+ // State will be updated via onAuthStateChange
109
+ } catch (err: any) {
110
+ const errorMessage = err.message || "Sign in failed";
111
+ setError(errorMessage);
112
+ throw err;
90
113
  }
91
- await service.signIn({ email, password });
92
- // State will be updated via onAuthStateChange
93
114
  }, []);
94
115
 
95
116
  const signOut = useCallback(async () => {
@@ -118,6 +139,7 @@ export function useAuth(): UseAuthResult {
118
139
  loading,
119
140
  isGuest,
120
141
  isAuthenticated: !!user && !isGuest,
142
+ error,
121
143
  signUp,
122
144
  signIn,
123
145
  signOut,
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Auth Navigator
3
+ * Stack navigator for authentication screens (Login, Register)
4
+ */
5
+
6
+ import React, { useEffect, useState } from "react";
7
+ import { createStackNavigator } from "@react-navigation/stack";
8
+ import { useAppDesignTokens } from "@umituz/react-native-theme";
9
+ import { storageRepository } from "@umituz/react-native-storage";
10
+ import { unwrap } from "@umituz/react-native-storage";
11
+ import { LoginScreen } from "../screens/LoginScreen";
12
+ import { RegisterScreen } from "../screens/RegisterScreen";
13
+
14
+ export type AuthStackParamList = {
15
+ Login: undefined;
16
+ Register: undefined;
17
+ };
18
+
19
+ const AuthStack = createStackNavigator<AuthStackParamList>();
20
+
21
+ const SHOW_REGISTER_KEY = "auth_show_register";
22
+
23
+ export const AuthNavigator: React.FC = () => {
24
+ const tokens = useAppDesignTokens();
25
+ const [initialRouteName, setInitialRouteName] = useState<
26
+ "Login" | "Register" | undefined
27
+ >(undefined);
28
+
29
+ useEffect(() => {
30
+ storageRepository.getString(SHOW_REGISTER_KEY, "false").then((result) => {
31
+ const value = unwrap(result, "false");
32
+ if (value === "true") {
33
+ setInitialRouteName("Register");
34
+ storageRepository.removeItem(SHOW_REGISTER_KEY);
35
+ } else {
36
+ setInitialRouteName("Login");
37
+ }
38
+ });
39
+ }, []);
40
+
41
+ if (initialRouteName === undefined) {
42
+ return null;
43
+ }
44
+
45
+ return (
46
+ <AuthStack.Navigator
47
+ initialRouteName={initialRouteName}
48
+ screenOptions={{
49
+ headerShown: false,
50
+ cardStyle: { backgroundColor: tokens.colors.backgroundPrimary },
51
+ }}
52
+ >
53
+ <AuthStack.Screen name="Login" component={LoginScreen} />
54
+ <AuthStack.Screen name="Register" component={RegisterScreen} />
55
+ </AuthStack.Navigator>
56
+ );
57
+ };
58
+
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Login Screen
3
+ * Beautiful, production-ready login screen with email/password and guest mode
4
+ */
5
+
6
+ import React from "react";
7
+ import { useNavigation } from "@react-navigation/native";
8
+ import { useLocalization } from "@umituz/react-native-localization";
9
+ import type { AuthStackParamList } from "../navigation/AuthNavigator";
10
+ import type { StackNavigationProp } from "@react-navigation/stack";
11
+ import { AuthContainer } from "../components/AuthContainer";
12
+ import { AuthHeader } from "../components/AuthHeader";
13
+ import { AuthFormCard } from "../components/AuthFormCard";
14
+ import { LoginForm } from "../components/LoginForm";
15
+
16
+ type LoginScreenNavigationProp = StackNavigationProp<
17
+ AuthStackParamList,
18
+ "Login"
19
+ >;
20
+
21
+ export const LoginScreen: React.FC = () => {
22
+ const { t } = useLocalization();
23
+ const navigation = useNavigation<LoginScreenNavigationProp>();
24
+
25
+ const handleNavigateToRegister = () => {
26
+ navigation.navigate("Register");
27
+ };
28
+
29
+ return (
30
+ <AuthContainer>
31
+ <AuthHeader title={t("auth.title")} />
32
+ <AuthFormCard>
33
+ <LoginForm onNavigateToRegister={handleNavigateToRegister} />
34
+ </AuthFormCard>
35
+ </AuthContainer>
36
+ );
37
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Register Screen
3
+ * Beautiful, production-ready registration screen with validation
4
+ */
5
+
6
+ import React from "react";
7
+ import { useNavigation } from "@react-navigation/native";
8
+ import { useLocalization } from "@umituz/react-native-localization";
9
+ import type { AuthStackParamList } from "../navigation/AuthNavigator";
10
+ import type { StackNavigationProp } from "@react-navigation/stack";
11
+ import { AuthContainer } from "../components/AuthContainer";
12
+ import { AuthHeader } from "../components/AuthHeader";
13
+ import { AuthFormCard } from "../components/AuthFormCard";
14
+ import { RegisterForm } from "../components/RegisterForm";
15
+
16
+ type RegisterScreenNavigationProp = StackNavigationProp<
17
+ AuthStackParamList,
18
+ "Register"
19
+ >;
20
+
21
+ export const RegisterScreen: React.FC = () => {
22
+ const { t } = useLocalization();
23
+ const navigation = useNavigation<RegisterScreenNavigationProp>();
24
+
25
+ const handleNavigateToLogin = () => {
26
+ navigation.navigate("Login");
27
+ };
28
+
29
+ return (
30
+ <AuthContainer>
31
+ <AuthHeader title={t("auth.createAccount")} />
32
+ <AuthFormCard>
33
+ <RegisterForm onNavigateToLogin={handleNavigateToLogin} />
34
+ </AuthFormCard>
35
+ </AuthContainer>
36
+ );
37
+ };