@umituz/react-native-auth 1.2.0 → 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.2.0",
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; // Added: Username support for app-specific needs
13
12
  }
14
13
 
15
14
  export interface SignInParams {
@@ -3,8 +3,6 @@
3
3
  * Validates and stores authentication configuration
4
4
  */
5
5
 
6
- import type { User } from "firebase/auth";
7
-
8
6
  export interface AuthConfig {
9
7
  /** Minimum password length (default: 6) */
10
8
  minPasswordLength?: number;
@@ -17,16 +15,14 @@ export interface AuthConfig {
17
15
  /** Require special characters in password */
18
16
  requireSpecialChars?: boolean;
19
17
  /** Callback for user profile creation after signup */
20
- onUserCreated?: (user: User, username?: string) => Promise<void> | void;
18
+ onUserCreated?: (user: any) => Promise<void> | void;
21
19
  /** Callback for user profile update */
22
- onUserUpdated?: (user: User) => Promise<void> | void;
20
+ onUserUpdated?: (user: any) => Promise<void> | void;
23
21
  /** Callback for sign out cleanup */
24
22
  onSignOut?: () => Promise<void> | void;
25
- /** Callback for account deletion (optional, for app-specific cleanup) */
26
- onAccountDeleted?: (userId: string) => Promise<void> | void;
27
23
  }
28
24
 
29
- export const DEFAULT_AUTH_CONFIG: Required<Omit<AuthConfig, 'onUserCreated' | 'onUserUpdated' | 'onSignOut' | 'onAccountDeleted'>> = {
25
+ export const DEFAULT_AUTH_CONFIG: Required<Omit<AuthConfig, 'onUserCreated' | 'onUserUpdated' | 'onSignOut'>> = {
30
26
  minPasswordLength: 6,
31
27
  requireUppercase: false,
32
28
  requireLowercase: false,
package/src/index.ts CHANGED
@@ -60,3 +60,12 @@ export {
60
60
  export { useAuth } from './presentation/hooks/useAuth';
61
61
  export type { UseAuthResult } from './presentation/hooks/useAuth';
62
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,
@@ -41,7 +40,7 @@ function validateEmail(email: string): boolean {
41
40
  */
42
41
  function validatePassword(
43
42
  password: string,
44
- config: Required<Omit<AuthConfig, "onUserCreated" | "onUserUpdated" | "onSignOut" | "onAccountDeleted">>
43
+ config: Required<Omit<AuthConfig, "onUserCreated" | "onUserUpdated" | "onSignOut">>
45
44
  ): { valid: boolean; error?: string } {
46
45
  if (password.length < config.minPasswordLength) {
47
46
  return {
@@ -83,13 +82,10 @@ function validatePassword(
83
82
 
84
83
  /**
85
84
  * Map Firebase Auth errors to domain errors
86
- * Type-safe error mapping
87
85
  */
88
- function mapFirebaseAuthError(error: unknown): Error {
89
- // Type guard for Firebase Auth errors
90
- const firebaseError = error as { code?: string; message?: string };
91
- const code = firebaseError?.code || "";
92
- const message = firebaseError?.message || "Authentication failed";
86
+ function mapFirebaseAuthError(error: any): Error {
87
+ const code = error?.code || "";
88
+ const message = error?.message || "Authentication failed";
93
89
 
94
90
  // Firebase Auth error codes
95
91
  if (code === "auth/email-already-in-use") {
@@ -154,7 +150,7 @@ export class AuthService implements IAuthService {
154
150
  private getAuth(): Auth | null {
155
151
  if (!this.auth) {
156
152
  /* eslint-disable-next-line no-console */
157
- if (typeof __DEV__ !== "undefined" && __DEV__) {
153
+ if (__DEV__) {
158
154
  console.warn("Auth service is not initialized. Call initialize() first.");
159
155
  }
160
156
  return null;
@@ -177,10 +173,7 @@ export class AuthService implements IAuthService {
177
173
  }
178
174
 
179
175
  // Validate password
180
- const passwordValidation = validatePassword(
181
- params.password,
182
- this.config as Required<Omit<AuthConfig, "onUserCreated" | "onUserUpdated" | "onSignOut" | "onAccountDeleted">>
183
- );
176
+ const passwordValidation = validatePassword(params.password, this.config as any);
184
177
  if (!passwordValidation.valid) {
185
178
  throw new AuthWeakPasswordError(passwordValidation.error);
186
179
  }
@@ -208,14 +201,14 @@ export class AuthService implements IAuthService {
208
201
  // Call user created callback if provided
209
202
  if (this.config.onUserCreated) {
210
203
  try {
211
- await this.config.onUserCreated(userCredential.user, params.username);
204
+ await this.config.onUserCreated(userCredential.user);
212
205
  } catch (callbackError) {
213
206
  // Don't fail signup if callback fails
214
207
  }
215
208
  }
216
209
 
217
210
  return userCredential.user;
218
- } catch (error: unknown) {
211
+ } catch (error: any) {
219
212
  throw mapFirebaseAuthError(error);
220
213
  }
221
214
  }
@@ -248,7 +241,7 @@ export class AuthService implements IAuthService {
248
241
 
249
242
  this.isGuestMode = false;
250
243
  return userCredential.user;
251
- } catch (error: unknown) {
244
+ } catch (error: any) {
252
245
  throw mapFirebaseAuthError(error);
253
246
  }
254
247
  }
@@ -276,7 +269,7 @@ export class AuthService implements IAuthService {
276
269
  // Don't fail signout if callback fails
277
270
  }
278
271
  }
279
- } catch (error: unknown) {
272
+ } catch (error: any) {
280
273
  throw mapFirebaseAuthError(error);
281
274
  }
282
275
  }
@@ -366,7 +359,7 @@ export function initializeAuthService(
366
359
  export function getAuthService(): AuthService | null {
367
360
  if (!authServiceInstance || !authServiceInstance.isInitialized()) {
368
361
  /* eslint-disable-next-line no-console */
369
- if (typeof __DEV__ !== "undefined" && __DEV__) {
362
+ if (__DEV__) {
370
363
  console.warn(
371
364
  "Auth service is not initialized. Call initializeAuthService() first."
372
365
  );
@@ -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
+ };