@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 +11 -2
- package/src/application/ports/IAuthService.ts +0 -1
- package/src/domain/errors/AuthError.ts +0 -8
- package/src/domain/value-objects/AuthConfig.ts +1 -5
- package/src/index.ts +9 -1
- package/src/infrastructure/services/AuthService.ts +1 -29
- 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.1
|
|
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",
|
|
@@ -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
|
|
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
|
-
|
|
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
|
+
};
|