@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 +11 -2
- package/src/application/ports/IAuthService.ts +0 -1
- package/src/domain/value-objects/AuthConfig.ts +3 -7
- package/src/index.ts +9 -0
- package/src/infrastructure/services/AuthService.ts +11 -18
- package/src/presentation/components/AuthContainer.tsx +61 -0
- package/src/presentation/components/AuthDivider.tsx +59 -0
- package/src/presentation/components/AuthErrorDisplay.tsx +43 -0
- package/src/presentation/components/AuthFormCard.tsx +38 -0
- package/src/presentation/components/AuthGradientBackground.tsx +23 -0
- package/src/presentation/components/AuthHeader.tsx +68 -0
- package/src/presentation/components/AuthLink.tsx +64 -0
- package/src/presentation/components/LoginForm.tsx +174 -0
- package/src/presentation/components/RegisterForm.tsx +225 -0
- package/src/presentation/hooks/useAuth.ts +28 -6
- package/src/presentation/navigation/AuthNavigator.tsx +58 -0
- package/src/presentation/screens/LoginScreen.tsx +37 -0
- package/src/presentation/screens/RegisterScreen.tsx +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-auth",
|
|
3
|
-
"version": "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",
|
|
@@ -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:
|
|
18
|
+
onUserCreated?: (user: any) => Promise<void> | void;
|
|
21
19
|
/** Callback for user profile update */
|
|
22
|
-
onUserUpdated?: (user:
|
|
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'
|
|
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"
|
|
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:
|
|
89
|
-
|
|
90
|
-
const
|
|
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 (
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|