@croacroa/react-native-template 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/.eslintrc.js +55 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/eas-build.yml +55 -0
- package/.github/workflows/eas-update.yml +50 -0
- package/.gitignore +62 -0
- package/.prettierrc +11 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.tsx +30 -0
- package/CHANGELOG.md +106 -0
- package/CONTRIBUTING.md +377 -0
- package/README.md +399 -0
- package/__tests__/components/Button.test.tsx +74 -0
- package/__tests__/hooks/useAuth.test.tsx +499 -0
- package/__tests__/services/api.test.ts +535 -0
- package/__tests__/utils/cn.test.ts +39 -0
- package/app/(auth)/_layout.tsx +36 -0
- package/app/(auth)/home.tsx +117 -0
- package/app/(auth)/profile.tsx +152 -0
- package/app/(auth)/settings.tsx +147 -0
- package/app/(public)/_layout.tsx +21 -0
- package/app/(public)/forgot-password.tsx +127 -0
- package/app/(public)/login.tsx +120 -0
- package/app/(public)/onboarding.tsx +5 -0
- package/app/(public)/register.tsx +139 -0
- package/app/_layout.tsx +97 -0
- package/app/index.tsx +21 -0
- package/app.config.ts +72 -0
- package/assets/images/.gitkeep +7 -0
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/babel.config.js +10 -0
- package/components/ErrorBoundary.tsx +169 -0
- package/components/forms/FormInput.tsx +78 -0
- package/components/forms/index.ts +1 -0
- package/components/onboarding/OnboardingScreen.tsx +370 -0
- package/components/onboarding/index.ts +2 -0
- package/components/ui/AnimatedButton.tsx +156 -0
- package/components/ui/AnimatedCard.tsx +108 -0
- package/components/ui/Avatar.tsx +316 -0
- package/components/ui/Badge.tsx +416 -0
- package/components/ui/BottomSheet.tsx +307 -0
- package/components/ui/Button.stories.tsx +115 -0
- package/components/ui/Button.tsx +104 -0
- package/components/ui/Card.stories.tsx +84 -0
- package/components/ui/Card.tsx +32 -0
- package/components/ui/Checkbox.tsx +261 -0
- package/components/ui/Input.stories.tsx +106 -0
- package/components/ui/Input.tsx +117 -0
- package/components/ui/Modal.tsx +98 -0
- package/components/ui/OptimizedImage.tsx +369 -0
- package/components/ui/Select.tsx +240 -0
- package/components/ui/Skeleton.tsx +180 -0
- package/components/ui/index.ts +18 -0
- package/constants/config.ts +54 -0
- package/docs/adr/001-state-management.md +79 -0
- package/docs/adr/002-styling-approach.md +130 -0
- package/docs/adr/003-data-fetching.md +155 -0
- package/docs/adr/004-auth-adapter-pattern.md +144 -0
- package/docs/adr/README.md +78 -0
- package/eas.json +47 -0
- package/global.css +10 -0
- package/hooks/index.ts +25 -0
- package/hooks/useApi.ts +236 -0
- package/hooks/useAuth.tsx +290 -0
- package/hooks/useBiometrics.ts +295 -0
- package/hooks/useDeepLinking.ts +256 -0
- package/hooks/useNotifications.ts +138 -0
- package/hooks/useOffline.ts +69 -0
- package/hooks/usePerformance.ts +434 -0
- package/hooks/useTheme.tsx +85 -0
- package/hooks/useUpdates.ts +358 -0
- package/i18n/index.ts +77 -0
- package/i18n/locales/en.json +101 -0
- package/i18n/locales/fr.json +101 -0
- package/jest.config.js +32 -0
- package/maestro/README.md +113 -0
- package/maestro/config.yaml +35 -0
- package/maestro/flows/login.yaml +62 -0
- package/maestro/flows/navigation.yaml +68 -0
- package/maestro/flows/offline.yaml +60 -0
- package/maestro/flows/register.yaml +94 -0
- package/metro.config.js +6 -0
- package/nativewind-env.d.ts +1 -0
- package/package.json +170 -0
- package/scripts/init.ps1 +162 -0
- package/scripts/init.sh +174 -0
- package/services/analytics.ts +428 -0
- package/services/api.ts +340 -0
- package/services/authAdapter.ts +333 -0
- package/services/index.ts +22 -0
- package/services/queryClient.ts +97 -0
- package/services/sentry.ts +131 -0
- package/services/storage.ts +82 -0
- package/stores/appStore.ts +54 -0
- package/stores/index.ts +2 -0
- package/stores/notificationStore.ts +40 -0
- package/tailwind.config.js +47 -0
- package/tsconfig.json +26 -0
- package/types/index.ts +42 -0
- package/types/user.ts +63 -0
- package/utils/accessibility.ts +446 -0
- package/utils/cn.ts +14 -0
- package/utils/index.ts +43 -0
- package/utils/toast.ts +113 -0
- package/utils/validation.ts +67 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { View, Text, ScrollView } from "react-native";
|
|
2
|
+
import { Link } from "expo-router";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
5
|
+
|
|
6
|
+
import { Card } from "@/components/ui/Card";
|
|
7
|
+
import { Button } from "@/components/ui/Button";
|
|
8
|
+
import { useAuth } from "@/hooks/useAuth";
|
|
9
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
10
|
+
|
|
11
|
+
export default function HomeScreen() {
|
|
12
|
+
const { user } = useAuth();
|
|
13
|
+
const { isDark } = useTheme();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
|
|
17
|
+
<ScrollView className="flex-1 px-4 pt-4">
|
|
18
|
+
{/* Header */}
|
|
19
|
+
<View className="mb-6 flex-row items-center justify-between">
|
|
20
|
+
<View>
|
|
21
|
+
<Text className="text-muted-light dark:text-muted-dark">
|
|
22
|
+
Welcome back,
|
|
23
|
+
</Text>
|
|
24
|
+
<Text className="text-2xl font-bold text-text-light dark:text-text-dark">
|
|
25
|
+
{user?.name || "User"}
|
|
26
|
+
</Text>
|
|
27
|
+
</View>
|
|
28
|
+
<Link href="/(auth)/profile" asChild>
|
|
29
|
+
<View className="h-12 w-12 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900">
|
|
30
|
+
<Ionicons
|
|
31
|
+
name="person"
|
|
32
|
+
size={24}
|
|
33
|
+
color={isDark ? "#93c5fd" : "#2563eb"}
|
|
34
|
+
/>
|
|
35
|
+
</View>
|
|
36
|
+
</Link>
|
|
37
|
+
</View>
|
|
38
|
+
|
|
39
|
+
{/* Quick Actions */}
|
|
40
|
+
<Text className="mb-3 text-lg font-semibold text-text-light dark:text-text-dark">
|
|
41
|
+
Quick Actions
|
|
42
|
+
</Text>
|
|
43
|
+
<View className="mb-6 flex-row gap-3">
|
|
44
|
+
<Card className="flex-1 items-center p-4">
|
|
45
|
+
<View className="mb-2 h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/30">
|
|
46
|
+
<Ionicons
|
|
47
|
+
name="add"
|
|
48
|
+
size={24}
|
|
49
|
+
color={isDark ? "#93c5fd" : "#2563eb"}
|
|
50
|
+
/>
|
|
51
|
+
</View>
|
|
52
|
+
<Text className="text-sm text-text-light dark:text-text-dark">
|
|
53
|
+
New Item
|
|
54
|
+
</Text>
|
|
55
|
+
</Card>
|
|
56
|
+
<Card className="flex-1 items-center p-4">
|
|
57
|
+
<View className="mb-2 h-12 w-12 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
|
58
|
+
<Ionicons
|
|
59
|
+
name="search"
|
|
60
|
+
size={24}
|
|
61
|
+
color={isDark ? "#86efac" : "#16a34a"}
|
|
62
|
+
/>
|
|
63
|
+
</View>
|
|
64
|
+
<Text className="text-sm text-text-light dark:text-text-dark">
|
|
65
|
+
Search
|
|
66
|
+
</Text>
|
|
67
|
+
</Card>
|
|
68
|
+
<Card className="flex-1 items-center p-4">
|
|
69
|
+
<View className="mb-2 h-12 w-12 items-center justify-center rounded-full bg-purple-100 dark:bg-purple-900/30">
|
|
70
|
+
<Ionicons
|
|
71
|
+
name="stats-chart"
|
|
72
|
+
size={24}
|
|
73
|
+
color={isDark ? "#c4b5fd" : "#7c3aed"}
|
|
74
|
+
/>
|
|
75
|
+
</View>
|
|
76
|
+
<Text className="text-sm text-text-light dark:text-text-dark">
|
|
77
|
+
Stats
|
|
78
|
+
</Text>
|
|
79
|
+
</Card>
|
|
80
|
+
</View>
|
|
81
|
+
|
|
82
|
+
{/* Recent Activity */}
|
|
83
|
+
<Text className="mb-3 text-lg font-semibold text-text-light dark:text-text-dark">
|
|
84
|
+
Recent Activity
|
|
85
|
+
</Text>
|
|
86
|
+
<Card className="mb-4 p-4">
|
|
87
|
+
<View className="items-center py-8">
|
|
88
|
+
<Ionicons
|
|
89
|
+
name="time-outline"
|
|
90
|
+
size={48}
|
|
91
|
+
color={isDark ? "#64748b" : "#94a3b8"}
|
|
92
|
+
/>
|
|
93
|
+
<Text className="mt-2 text-muted-light dark:text-muted-dark">
|
|
94
|
+
No recent activity
|
|
95
|
+
</Text>
|
|
96
|
+
<Text className="mt-1 text-sm text-muted-light dark:text-muted-dark">
|
|
97
|
+
Your activity will appear here
|
|
98
|
+
</Text>
|
|
99
|
+
</View>
|
|
100
|
+
</Card>
|
|
101
|
+
|
|
102
|
+
{/* Settings Link */}
|
|
103
|
+
<Link href="/(auth)/settings" asChild>
|
|
104
|
+
<Button variant="outline" className="mb-8">
|
|
105
|
+
<Ionicons
|
|
106
|
+
name="settings-outline"
|
|
107
|
+
size={20}
|
|
108
|
+
color={isDark ? "#f8fafc" : "#0f172a"}
|
|
109
|
+
style={{ marginRight: 8 }}
|
|
110
|
+
/>
|
|
111
|
+
Settings
|
|
112
|
+
</Button>
|
|
113
|
+
</Link>
|
|
114
|
+
</ScrollView>
|
|
115
|
+
</SafeAreaView>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { View, Text, ScrollView, Pressable } from "react-native";
|
|
2
|
+
import { router } from "expo-router";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
5
|
+
|
|
6
|
+
import { Card } from "@/components/ui/Card";
|
|
7
|
+
import { Button } from "@/components/ui/Button";
|
|
8
|
+
import { useAuth } from "@/hooks/useAuth";
|
|
9
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
10
|
+
|
|
11
|
+
export default function ProfileScreen() {
|
|
12
|
+
const { user, signOut } = useAuth();
|
|
13
|
+
const { isDark } = useTheme();
|
|
14
|
+
|
|
15
|
+
const handleSignOut = async () => {
|
|
16
|
+
await signOut();
|
|
17
|
+
router.replace("/(public)/login");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
|
|
22
|
+
<ScrollView className="flex-1">
|
|
23
|
+
{/* Header */}
|
|
24
|
+
<View className="flex-row items-center px-4 py-2">
|
|
25
|
+
<Pressable onPress={() => router.back()} className="mr-4">
|
|
26
|
+
<Ionicons
|
|
27
|
+
name="arrow-back"
|
|
28
|
+
size={24}
|
|
29
|
+
color={isDark ? "#f8fafc" : "#0f172a"}
|
|
30
|
+
/>
|
|
31
|
+
</Pressable>
|
|
32
|
+
<Text className="text-xl font-semibold text-text-light dark:text-text-dark">
|
|
33
|
+
Profile
|
|
34
|
+
</Text>
|
|
35
|
+
</View>
|
|
36
|
+
|
|
37
|
+
{/* Avatar */}
|
|
38
|
+
<View className="items-center py-8">
|
|
39
|
+
<View className="mb-4 h-24 w-24 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900">
|
|
40
|
+
<Text className="text-4xl font-bold text-primary-600 dark:text-primary-400">
|
|
41
|
+
{user?.name?.charAt(0).toUpperCase() || "U"}
|
|
42
|
+
</Text>
|
|
43
|
+
</View>
|
|
44
|
+
<Text className="text-2xl font-bold text-text-light dark:text-text-dark">
|
|
45
|
+
{user?.name || "User"}
|
|
46
|
+
</Text>
|
|
47
|
+
<Text className="text-muted-light dark:text-muted-dark">
|
|
48
|
+
{user?.email || "user@example.com"}
|
|
49
|
+
</Text>
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
{/* Info Cards */}
|
|
53
|
+
<View className="px-4">
|
|
54
|
+
<Card className="mb-4">
|
|
55
|
+
<Pressable className="flex-row items-center justify-between p-4">
|
|
56
|
+
<View className="flex-row items-center">
|
|
57
|
+
<Ionicons
|
|
58
|
+
name="person-outline"
|
|
59
|
+
size={24}
|
|
60
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
61
|
+
/>
|
|
62
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
63
|
+
Edit Profile
|
|
64
|
+
</Text>
|
|
65
|
+
</View>
|
|
66
|
+
<Ionicons
|
|
67
|
+
name="chevron-forward"
|
|
68
|
+
size={20}
|
|
69
|
+
color={isDark ? "#64748b" : "#94a3b8"}
|
|
70
|
+
/>
|
|
71
|
+
</Pressable>
|
|
72
|
+
</Card>
|
|
73
|
+
|
|
74
|
+
<Card className="mb-4">
|
|
75
|
+
<Pressable className="flex-row items-center justify-between p-4">
|
|
76
|
+
<View className="flex-row items-center">
|
|
77
|
+
<Ionicons
|
|
78
|
+
name="notifications-outline"
|
|
79
|
+
size={24}
|
|
80
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
81
|
+
/>
|
|
82
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
83
|
+
Notifications
|
|
84
|
+
</Text>
|
|
85
|
+
</View>
|
|
86
|
+
<Ionicons
|
|
87
|
+
name="chevron-forward"
|
|
88
|
+
size={20}
|
|
89
|
+
color={isDark ? "#64748b" : "#94a3b8"}
|
|
90
|
+
/>
|
|
91
|
+
</Pressable>
|
|
92
|
+
</Card>
|
|
93
|
+
|
|
94
|
+
<Card className="mb-4">
|
|
95
|
+
<Pressable className="flex-row items-center justify-between p-4">
|
|
96
|
+
<View className="flex-row items-center">
|
|
97
|
+
<Ionicons
|
|
98
|
+
name="shield-outline"
|
|
99
|
+
size={24}
|
|
100
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
101
|
+
/>
|
|
102
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
103
|
+
Privacy & Security
|
|
104
|
+
</Text>
|
|
105
|
+
</View>
|
|
106
|
+
<Ionicons
|
|
107
|
+
name="chevron-forward"
|
|
108
|
+
size={20}
|
|
109
|
+
color={isDark ? "#64748b" : "#94a3b8"}
|
|
110
|
+
/>
|
|
111
|
+
</Pressable>
|
|
112
|
+
</Card>
|
|
113
|
+
|
|
114
|
+
<Card className="mb-4">
|
|
115
|
+
<Pressable className="flex-row items-center justify-between p-4">
|
|
116
|
+
<View className="flex-row items-center">
|
|
117
|
+
<Ionicons
|
|
118
|
+
name="help-circle-outline"
|
|
119
|
+
size={24}
|
|
120
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
121
|
+
/>
|
|
122
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
123
|
+
Help & Support
|
|
124
|
+
</Text>
|
|
125
|
+
</View>
|
|
126
|
+
<Ionicons
|
|
127
|
+
name="chevron-forward"
|
|
128
|
+
size={20}
|
|
129
|
+
color={isDark ? "#64748b" : "#94a3b8"}
|
|
130
|
+
/>
|
|
131
|
+
</Pressable>
|
|
132
|
+
</Card>
|
|
133
|
+
|
|
134
|
+
{/* Sign Out */}
|
|
135
|
+
<Button
|
|
136
|
+
variant="outline"
|
|
137
|
+
onPress={handleSignOut}
|
|
138
|
+
className="mt-4 mb-8 border-red-500"
|
|
139
|
+
>
|
|
140
|
+
<Ionicons
|
|
141
|
+
name="log-out-outline"
|
|
142
|
+
size={20}
|
|
143
|
+
color="#ef4444"
|
|
144
|
+
style={{ marginRight: 8 }}
|
|
145
|
+
/>
|
|
146
|
+
<Text className="text-red-500">Sign Out</Text>
|
|
147
|
+
</Button>
|
|
148
|
+
</View>
|
|
149
|
+
</ScrollView>
|
|
150
|
+
</SafeAreaView>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { View, Text, ScrollView, Pressable, Switch } from "react-native";
|
|
2
|
+
import { router } from "expo-router";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
5
|
+
|
|
6
|
+
import { Card } from "@/components/ui/Card";
|
|
7
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
8
|
+
import { useNotificationStore } from "@/stores/notificationStore";
|
|
9
|
+
|
|
10
|
+
export default function SettingsScreen() {
|
|
11
|
+
const { isDark, toggleTheme } = useTheme();
|
|
12
|
+
const { isEnabled, toggleNotifications } = useNotificationStore();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
|
|
16
|
+
<ScrollView className="flex-1">
|
|
17
|
+
{/* Header */}
|
|
18
|
+
<View className="flex-row items-center px-4 py-2">
|
|
19
|
+
<Pressable onPress={() => router.back()} className="mr-4">
|
|
20
|
+
<Ionicons
|
|
21
|
+
name="arrow-back"
|
|
22
|
+
size={24}
|
|
23
|
+
color={isDark ? "#f8fafc" : "#0f172a"}
|
|
24
|
+
/>
|
|
25
|
+
</Pressable>
|
|
26
|
+
<Text className="text-xl font-semibold text-text-light dark:text-text-dark">
|
|
27
|
+
Settings
|
|
28
|
+
</Text>
|
|
29
|
+
</View>
|
|
30
|
+
|
|
31
|
+
<View className="px-4 pt-4">
|
|
32
|
+
{/* Appearance */}
|
|
33
|
+
<Text className="mb-3 text-sm font-medium uppercase text-muted-light dark:text-muted-dark">
|
|
34
|
+
Appearance
|
|
35
|
+
</Text>
|
|
36
|
+
<Card className="mb-6">
|
|
37
|
+
<View className="flex-row items-center justify-between p-4">
|
|
38
|
+
<View className="flex-row items-center">
|
|
39
|
+
<Ionicons
|
|
40
|
+
name={isDark ? "moon" : "sunny"}
|
|
41
|
+
size={24}
|
|
42
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
43
|
+
/>
|
|
44
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
45
|
+
Dark Mode
|
|
46
|
+
</Text>
|
|
47
|
+
</View>
|
|
48
|
+
<Switch
|
|
49
|
+
value={isDark}
|
|
50
|
+
onValueChange={toggleTheme}
|
|
51
|
+
trackColor={{ false: "#cbd5e1", true: "#3b82f6" }}
|
|
52
|
+
thumbColor="#ffffff"
|
|
53
|
+
/>
|
|
54
|
+
</View>
|
|
55
|
+
</Card>
|
|
56
|
+
|
|
57
|
+
{/* Notifications */}
|
|
58
|
+
<Text className="mb-3 text-sm font-medium uppercase text-muted-light dark:text-muted-dark">
|
|
59
|
+
Notifications
|
|
60
|
+
</Text>
|
|
61
|
+
<Card className="mb-6">
|
|
62
|
+
<View className="flex-row items-center justify-between p-4">
|
|
63
|
+
<View className="flex-row items-center">
|
|
64
|
+
<Ionicons
|
|
65
|
+
name="notifications-outline"
|
|
66
|
+
size={24}
|
|
67
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
68
|
+
/>
|
|
69
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
70
|
+
Push Notifications
|
|
71
|
+
</Text>
|
|
72
|
+
</View>
|
|
73
|
+
<Switch
|
|
74
|
+
value={isEnabled}
|
|
75
|
+
onValueChange={toggleNotifications}
|
|
76
|
+
trackColor={{ false: "#cbd5e1", true: "#3b82f6" }}
|
|
77
|
+
thumbColor="#ffffff"
|
|
78
|
+
/>
|
|
79
|
+
</View>
|
|
80
|
+
</Card>
|
|
81
|
+
|
|
82
|
+
{/* About */}
|
|
83
|
+
<Text className="mb-3 text-sm font-medium uppercase text-muted-light dark:text-muted-dark">
|
|
84
|
+
About
|
|
85
|
+
</Text>
|
|
86
|
+
<Card className="mb-6">
|
|
87
|
+
<Pressable className="flex-row items-center justify-between p-4">
|
|
88
|
+
<View className="flex-row items-center">
|
|
89
|
+
<Ionicons
|
|
90
|
+
name="information-circle-outline"
|
|
91
|
+
size={24}
|
|
92
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
93
|
+
/>
|
|
94
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
95
|
+
App Version
|
|
96
|
+
</Text>
|
|
97
|
+
</View>
|
|
98
|
+
<Text className="text-muted-light dark:text-muted-dark">
|
|
99
|
+
1.0.0
|
|
100
|
+
</Text>
|
|
101
|
+
</Pressable>
|
|
102
|
+
|
|
103
|
+
<View className="mx-4 h-px bg-gray-200 dark:bg-gray-700" />
|
|
104
|
+
|
|
105
|
+
<Pressable className="flex-row items-center justify-between p-4">
|
|
106
|
+
<View className="flex-row items-center">
|
|
107
|
+
<Ionicons
|
|
108
|
+
name="document-text-outline"
|
|
109
|
+
size={24}
|
|
110
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
111
|
+
/>
|
|
112
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
113
|
+
Terms of Service
|
|
114
|
+
</Text>
|
|
115
|
+
</View>
|
|
116
|
+
<Ionicons
|
|
117
|
+
name="chevron-forward"
|
|
118
|
+
size={20}
|
|
119
|
+
color={isDark ? "#64748b" : "#94a3b8"}
|
|
120
|
+
/>
|
|
121
|
+
</Pressable>
|
|
122
|
+
|
|
123
|
+
<View className="mx-4 h-px bg-gray-200 dark:bg-gray-700" />
|
|
124
|
+
|
|
125
|
+
<Pressable className="flex-row items-center justify-between p-4">
|
|
126
|
+
<View className="flex-row items-center">
|
|
127
|
+
<Ionicons
|
|
128
|
+
name="lock-closed-outline"
|
|
129
|
+
size={24}
|
|
130
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
131
|
+
/>
|
|
132
|
+
<Text className="ml-3 text-text-light dark:text-text-dark">
|
|
133
|
+
Privacy Policy
|
|
134
|
+
</Text>
|
|
135
|
+
</View>
|
|
136
|
+
<Ionicons
|
|
137
|
+
name="chevron-forward"
|
|
138
|
+
size={20}
|
|
139
|
+
color={isDark ? "#64748b" : "#94a3b8"}
|
|
140
|
+
/>
|
|
141
|
+
</Pressable>
|
|
142
|
+
</Card>
|
|
143
|
+
</View>
|
|
144
|
+
</ScrollView>
|
|
145
|
+
</SafeAreaView>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Stack } from "expo-router";
|
|
2
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
3
|
+
|
|
4
|
+
export default function PublicLayout() {
|
|
5
|
+
const { isDark } = useTheme();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Stack
|
|
9
|
+
screenOptions={{
|
|
10
|
+
headerShown: false,
|
|
11
|
+
contentStyle: {
|
|
12
|
+
backgroundColor: isDark ? "#0f172a" : "#ffffff",
|
|
13
|
+
},
|
|
14
|
+
}}
|
|
15
|
+
>
|
|
16
|
+
<Stack.Screen name="login" />
|
|
17
|
+
<Stack.Screen name="register" />
|
|
18
|
+
<Stack.Screen name="forgot-password" />
|
|
19
|
+
</Stack>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { View, Text, Pressable, KeyboardAvoidingView, Platform } from "react-native";
|
|
3
|
+
import { Link, router } from "expo-router";
|
|
4
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
5
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
6
|
+
|
|
7
|
+
import { Input } from "@/components/ui/Input";
|
|
8
|
+
import { Button } from "@/components/ui/Button";
|
|
9
|
+
|
|
10
|
+
export default function ForgotPasswordScreen() {
|
|
11
|
+
const [email, setEmail] = useState("");
|
|
12
|
+
const [error, setError] = useState("");
|
|
13
|
+
const [success, setSuccess] = useState(false);
|
|
14
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleResetPassword = async () => {
|
|
17
|
+
if (!email) {
|
|
18
|
+
setError("Please enter your email");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setIsLoading(true);
|
|
23
|
+
setError("");
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// TODO: Implement password reset API call
|
|
27
|
+
// await api.resetPassword(email);
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
29
|
+
setSuccess(true);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
setError("Failed to send reset email. Please try again.");
|
|
32
|
+
} finally {
|
|
33
|
+
setIsLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (success) {
|
|
38
|
+
return (
|
|
39
|
+
<SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
|
|
40
|
+
<View className="flex-1 items-center justify-center px-6">
|
|
41
|
+
<View className="mb-6 h-20 w-20 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
|
42
|
+
<Ionicons name="mail-outline" size={40} color="#22c55e" />
|
|
43
|
+
</View>
|
|
44
|
+
<Text className="text-center text-2xl font-bold text-text-light dark:text-text-dark">
|
|
45
|
+
Check your email
|
|
46
|
+
</Text>
|
|
47
|
+
<Text className="mt-2 text-center text-muted-light dark:text-muted-dark">
|
|
48
|
+
We've sent a password reset link to {email}
|
|
49
|
+
</Text>
|
|
50
|
+
<Button
|
|
51
|
+
onPress={() => router.replace("/(public)/login")}
|
|
52
|
+
className="mt-8"
|
|
53
|
+
>
|
|
54
|
+
Back to Sign In
|
|
55
|
+
</Button>
|
|
56
|
+
</View>
|
|
57
|
+
</SafeAreaView>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
|
|
63
|
+
<KeyboardAvoidingView
|
|
64
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
65
|
+
className="flex-1"
|
|
66
|
+
>
|
|
67
|
+
<View className="flex-1 justify-center px-6">
|
|
68
|
+
<Pressable
|
|
69
|
+
onPress={() => router.back()}
|
|
70
|
+
className="mb-8 flex-row items-center"
|
|
71
|
+
>
|
|
72
|
+
<Ionicons name="arrow-back" size={24} color="#64748b" />
|
|
73
|
+
<Text className="ml-2 text-muted-light dark:text-muted-dark">Back</Text>
|
|
74
|
+
</Pressable>
|
|
75
|
+
|
|
76
|
+
<View className="mb-8">
|
|
77
|
+
<Text className="text-3xl font-bold text-text-light dark:text-text-dark">
|
|
78
|
+
Reset password
|
|
79
|
+
</Text>
|
|
80
|
+
<Text className="mt-2 text-muted-light dark:text-muted-dark">
|
|
81
|
+
Enter your email and we'll send you a reset link
|
|
82
|
+
</Text>
|
|
83
|
+
</View>
|
|
84
|
+
|
|
85
|
+
{error ? (
|
|
86
|
+
<View className="mb-4 rounded-lg bg-red-100 p-3 dark:bg-red-900/30">
|
|
87
|
+
<Text className="text-red-600 dark:text-red-400">{error}</Text>
|
|
88
|
+
</View>
|
|
89
|
+
) : null}
|
|
90
|
+
|
|
91
|
+
<View className="gap-4">
|
|
92
|
+
<Input
|
|
93
|
+
label="Email"
|
|
94
|
+
placeholder="Enter your email"
|
|
95
|
+
value={email}
|
|
96
|
+
onChangeText={setEmail}
|
|
97
|
+
keyboardType="email-address"
|
|
98
|
+
autoCapitalize="none"
|
|
99
|
+
autoComplete="email"
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<Button
|
|
103
|
+
onPress={handleResetPassword}
|
|
104
|
+
isLoading={isLoading}
|
|
105
|
+
className="mt-4"
|
|
106
|
+
>
|
|
107
|
+
Send Reset Link
|
|
108
|
+
</Button>
|
|
109
|
+
</View>
|
|
110
|
+
|
|
111
|
+
<View className="mt-8 flex-row justify-center">
|
|
112
|
+
<Text className="text-muted-light dark:text-muted-dark">
|
|
113
|
+
Remember your password?{" "}
|
|
114
|
+
</Text>
|
|
115
|
+
<Link href="/(public)/login" asChild>
|
|
116
|
+
<Pressable>
|
|
117
|
+
<Text className="font-semibold text-primary-600 dark:text-primary-400">
|
|
118
|
+
Sign In
|
|
119
|
+
</Text>
|
|
120
|
+
</Pressable>
|
|
121
|
+
</Link>
|
|
122
|
+
</View>
|
|
123
|
+
</View>
|
|
124
|
+
</KeyboardAvoidingView>
|
|
125
|
+
</SafeAreaView>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { View, Text, Pressable, KeyboardAvoidingView, Platform } from "react-native";
|
|
2
|
+
import { Link, router } from "expo-router";
|
|
3
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
import { useForm } from "react-hook-form";
|
|
5
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
6
|
+
import Animated, { FadeInDown } from "react-native-reanimated";
|
|
7
|
+
|
|
8
|
+
import { FormInput } from "@/components/forms/FormInput";
|
|
9
|
+
import { AnimatedButton } from "@/components/ui/AnimatedButton";
|
|
10
|
+
import { useAuth } from "@/hooks/useAuth";
|
|
11
|
+
import { loginSchema, LoginFormData } from "@/utils/validation";
|
|
12
|
+
|
|
13
|
+
export default function LoginScreen() {
|
|
14
|
+
const { signIn } = useAuth();
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
control,
|
|
18
|
+
handleSubmit,
|
|
19
|
+
formState: { isSubmitting },
|
|
20
|
+
} = useForm<LoginFormData>({
|
|
21
|
+
resolver: zodResolver(loginSchema),
|
|
22
|
+
defaultValues: {
|
|
23
|
+
email: "",
|
|
24
|
+
password: "",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const onSubmit = async (data: LoginFormData) => {
|
|
29
|
+
try {
|
|
30
|
+
await signIn(data.email, data.password);
|
|
31
|
+
router.replace("/(auth)/home");
|
|
32
|
+
} catch {
|
|
33
|
+
// Error is handled by useAuth with toast
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
|
|
39
|
+
<KeyboardAvoidingView
|
|
40
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
41
|
+
className="flex-1"
|
|
42
|
+
>
|
|
43
|
+
<View className="flex-1 justify-center px-6">
|
|
44
|
+
{/* Header */}
|
|
45
|
+
<Animated.View
|
|
46
|
+
entering={FadeInDown.delay(100).springify()}
|
|
47
|
+
className="mb-8"
|
|
48
|
+
>
|
|
49
|
+
<Text className="text-3xl font-bold text-text-light dark:text-text-dark">
|
|
50
|
+
Welcome back
|
|
51
|
+
</Text>
|
|
52
|
+
<Text className="mt-2 text-muted-light dark:text-muted-dark">
|
|
53
|
+
Sign in to your account
|
|
54
|
+
</Text>
|
|
55
|
+
</Animated.View>
|
|
56
|
+
|
|
57
|
+
{/* Form */}
|
|
58
|
+
<Animated.View
|
|
59
|
+
entering={FadeInDown.delay(200).springify()}
|
|
60
|
+
className="gap-4"
|
|
61
|
+
>
|
|
62
|
+
<FormInput
|
|
63
|
+
name="email"
|
|
64
|
+
control={control}
|
|
65
|
+
label="Email"
|
|
66
|
+
placeholder="Enter your email"
|
|
67
|
+
keyboardType="email-address"
|
|
68
|
+
autoCapitalize="none"
|
|
69
|
+
autoComplete="email"
|
|
70
|
+
leftIcon="mail-outline"
|
|
71
|
+
/>
|
|
72
|
+
|
|
73
|
+
<FormInput
|
|
74
|
+
name="password"
|
|
75
|
+
control={control}
|
|
76
|
+
label="Password"
|
|
77
|
+
placeholder="Enter your password"
|
|
78
|
+
secureTextEntry
|
|
79
|
+
autoComplete="password"
|
|
80
|
+
leftIcon="lock-closed-outline"
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
<Link href="/(public)/forgot-password" asChild>
|
|
84
|
+
<Pressable className="self-end">
|
|
85
|
+
<Text className="text-primary-600 dark:text-primary-400">
|
|
86
|
+
Forgot password?
|
|
87
|
+
</Text>
|
|
88
|
+
</Pressable>
|
|
89
|
+
</Link>
|
|
90
|
+
|
|
91
|
+
<AnimatedButton
|
|
92
|
+
onPress={handleSubmit(onSubmit)}
|
|
93
|
+
isLoading={isSubmitting}
|
|
94
|
+
className="mt-4"
|
|
95
|
+
>
|
|
96
|
+
Sign In
|
|
97
|
+
</AnimatedButton>
|
|
98
|
+
</Animated.View>
|
|
99
|
+
|
|
100
|
+
{/* Footer */}
|
|
101
|
+
<Animated.View
|
|
102
|
+
entering={FadeInDown.delay(300).springify()}
|
|
103
|
+
className="mt-8 flex-row justify-center"
|
|
104
|
+
>
|
|
105
|
+
<Text className="text-muted-light dark:text-muted-dark">
|
|
106
|
+
Don't have an account?{" "}
|
|
107
|
+
</Text>
|
|
108
|
+
<Link href="/(public)/register" asChild>
|
|
109
|
+
<Pressable>
|
|
110
|
+
<Text className="font-semibold text-primary-600 dark:text-primary-400">
|
|
111
|
+
Sign Up
|
|
112
|
+
</Text>
|
|
113
|
+
</Pressable>
|
|
114
|
+
</Link>
|
|
115
|
+
</Animated.View>
|
|
116
|
+
</View>
|
|
117
|
+
</KeyboardAvoidingView>
|
|
118
|
+
</SafeAreaView>
|
|
119
|
+
);
|
|
120
|
+
}
|