@highbeek/create-rnstarterkit 1.0.2-beta.7 → 1.1.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/LICENSE +21 -0
- package/README.md +270 -0
- package/dist/bin/create-rnstarterkit.js +205 -7
- package/dist/src/generators/appGenerator.js +1949 -127
- package/dist/src/generators/codeGenerator.js +289 -0
- package/dist/templates/cli-base/App.tsx +199 -21
- package/dist/templates/cli-base/assets/images/icon.png +0 -0
- package/dist/templates/cli-base/assets/images/partial-react-logo.png +0 -0
- package/dist/templates/cli-base/assets/images/react-logo.png +0 -0
- package/dist/templates/cli-base/babel.config.js +1 -0
- package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +6 -0
- package/dist/templates/cli-base/ios/Podfile +5 -0
- package/dist/templates/cli-base/jest.config.js +4 -0
- package/dist/templates/cli-base/package.json +8 -5
- package/dist/templates/cli-base/tsconfig.json +1 -0
- package/dist/templates/expo-base/app/_layout.tsx +7 -3
- package/dist/templates/expo-base/app/home.tsx +37 -0
- package/dist/templates/expo-base/app/index.tsx +166 -5
- package/dist/templates/expo-base/app.json +1 -2
- package/dist/templates/expo-base/package.json +5 -2
- package/dist/templates/optional/auth-context/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +33 -5
- package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +41 -6
- package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +41 -6
- package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-redux/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +39 -2
- package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +42 -7
- package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +7 -10
- package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +61 -11
- package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +6 -9
- package/dist/templates/optional/auth-redux/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-zustand/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +34 -6
- package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +41 -6
- package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +41 -6
- package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/ci/.github/workflows/ci.yml +32 -0
- package/dist/templates/optional/error-boundary/components/ErrorBoundary.tsx +83 -0
- package/dist/templates/optional/formik/components/formik/FormikInput.tsx +45 -0
- package/dist/templates/optional/formik/components/formik/LoginForm.tsx +60 -0
- package/dist/templates/optional/formik/schemas/auth.schema.ts +17 -0
- package/dist/templates/optional/i18n/src/i18n/hooks/useAppTranslation.ts +28 -0
- package/dist/templates/optional/i18n/src/i18n/i18n.ts +30 -0
- package/dist/templates/optional/i18n/src/i18n/locales/en.json +32 -0
- package/dist/templates/optional/i18n/src/i18n/locales/es.json +32 -0
- package/dist/templates/optional/maestro/.maestro/flows/01_welcome.yaml +5 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/01_welcome.yaml +5 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/02_login.yaml +13 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/03_logout.yaml +16 -0
- package/dist/templates/optional/mmkv/utils/storage.ts +17 -0
- package/dist/templates/optional/react-hook-form/components/rhf/LoginForm.tsx +63 -0
- package/dist/templates/optional/react-hook-form/components/rhf/RHFInput.tsx +50 -0
- package/dist/templates/optional/react-hook-form/schemas/auth.schema.ts +29 -0
- package/dist/templates/optional/react-query/hooks/useAppMutation.ts +16 -0
- package/dist/templates/optional/react-query/hooks/useAppQuery.ts +12 -0
- package/dist/templates/optional/react-query/services/queryClient.ts +14 -0
- package/dist/templates/optional/redux/store/hooks.ts +6 -0
- package/dist/templates/optional/redux/store/store.ts +11 -0
- package/dist/templates/optional/sentry/src/utils/sentry.ts +24 -0
- package/dist/templates/optional/swr/hooks/useSWRFetch.ts +14 -0
- package/dist/templates/optional/swr/providers/SWRProvider.tsx +21 -0
- package/dist/templates/optional/tsconfig.json +17 -0
- package/dist/templates/optional/zustand/store/appStore.ts +13 -0
- package/package.json +40 -5
- package/dist/templates/expo-base/components/ui/collapsible.tsx +0 -45
- package/dist/templates/expo-base/components/ui/icon-symbol.ios.tsx +0 -32
- package/dist/templates/expo-base/components/ui/icon-symbol.tsx +0 -41
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
3
4
|
|
|
4
5
|
export default function ProfileScreen() {
|
|
5
6
|
return (
|
|
6
|
-
<
|
|
7
|
+
<ScreenLayout>
|
|
7
8
|
<Text>This is the Profile Screen</Text>
|
|
8
|
-
</
|
|
9
|
+
</ScreenLayout>
|
|
9
10
|
);
|
|
10
11
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { TextInput, Button, Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
3
4
|
import { registerApi } from "../api";
|
|
4
5
|
import { useAuthStore } from "../store/authStore";
|
|
5
6
|
|
|
@@ -7,22 +8,56 @@ export default function RegisterScreen() {
|
|
|
7
8
|
const login = useAuthStore((state) => state.login);
|
|
8
9
|
const [email, setEmail] = useState("");
|
|
9
10
|
const [password, setPassword] = useState("");
|
|
11
|
+
const [error, setError] = useState("");
|
|
10
12
|
|
|
11
13
|
const handleRegister = async () => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
try {
|
|
15
|
+
setError("");
|
|
16
|
+
const token = await registerApi(email, password);
|
|
17
|
+
await login(token);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
setError(err instanceof Error ? err.message : "Registration failed");
|
|
20
|
+
}
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
return (
|
|
17
|
-
<
|
|
18
|
-
<TextInput
|
|
24
|
+
<ScreenLayout>
|
|
25
|
+
<TextInput
|
|
26
|
+
placeholder="Email"
|
|
27
|
+
value={email}
|
|
28
|
+
onChangeText={setEmail}
|
|
29
|
+
autoCapitalize="none"
|
|
30
|
+
autoCorrect={false}
|
|
31
|
+
keyboardType="email-address"
|
|
32
|
+
placeholderTextColor="#888"
|
|
33
|
+
style={{
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderColor: "#ccc",
|
|
36
|
+
borderRadius: 8,
|
|
37
|
+
paddingHorizontal: 12,
|
|
38
|
+
paddingVertical: 10,
|
|
39
|
+
color: "#111",
|
|
40
|
+
marginBottom: 12,
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
19
43
|
<TextInput
|
|
20
44
|
placeholder="Password"
|
|
21
45
|
value={password}
|
|
22
46
|
onChangeText={setPassword}
|
|
23
47
|
secureTextEntry
|
|
48
|
+
placeholderTextColor="#888"
|
|
49
|
+
style={{
|
|
50
|
+
borderWidth: 1,
|
|
51
|
+
borderColor: "#ccc",
|
|
52
|
+
borderRadius: 8,
|
|
53
|
+
paddingHorizontal: 12,
|
|
54
|
+
paddingVertical: 10,
|
|
55
|
+
color: "#111",
|
|
56
|
+
marginBottom: 12,
|
|
57
|
+
}}
|
|
24
58
|
/>
|
|
59
|
+
{!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
|
|
25
60
|
<Button title="Register" onPress={() => void handleRegister()} />
|
|
26
|
-
</
|
|
61
|
+
</ScreenLayout>
|
|
27
62
|
);
|
|
28
63
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
3
4
|
|
|
4
5
|
export default function SettingsScreen() {
|
|
5
6
|
return (
|
|
6
|
-
<
|
|
7
|
+
<ScreenLayout>
|
|
7
8
|
<Text>This is the Settings Screen</Text>
|
|
8
|
-
</
|
|
9
|
+
</ScreenLayout>
|
|
9
10
|
);
|
|
10
11
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
Image,
|
|
5
|
+
ImageSourcePropType,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
useWindowDimensions,
|
|
10
|
+
View,
|
|
11
|
+
ViewToken,
|
|
12
|
+
} from "react-native";
|
|
13
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
14
|
+
|
|
15
|
+
type WelcomeScreenProps = {
|
|
16
|
+
onContinue: () => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Slide = {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
image: ImageSourcePropType;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const slides: Slide[] = [
|
|
27
|
+
{
|
|
28
|
+
id: "1",
|
|
29
|
+
title: "Welcome to RN Starter Kit",
|
|
30
|
+
description: "Build faster with a clean, production-ready app foundation.",
|
|
31
|
+
image: require("../assets/images/react-logo.png"),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "2",
|
|
35
|
+
title: "Structure That Scales",
|
|
36
|
+
description: "Auth, state, and API setup are organized for real-world apps.",
|
|
37
|
+
image: require("../assets/images/partial-react-logo.png"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "3",
|
|
41
|
+
title: "Make It Yours",
|
|
42
|
+
description: "Swap this welcome flow with your own brand and product journey.",
|
|
43
|
+
image: require("../assets/images/icon.png"),
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export default function WelcomeScreen({ onContinue }: WelcomeScreenProps) {
|
|
48
|
+
const { width } = useWindowDimensions();
|
|
49
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
50
|
+
const listRef = useRef<FlatList<Slide>>(null);
|
|
51
|
+
|
|
52
|
+
const onViewableItemsChanged = useRef(
|
|
53
|
+
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
|
54
|
+
if (viewableItems[0]?.index != null) {
|
|
55
|
+
setActiveIndex(viewableItems[0].index);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
).current;
|
|
59
|
+
|
|
60
|
+
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }).current;
|
|
61
|
+
|
|
62
|
+
const handleNext = () => {
|
|
63
|
+
const nextIndex = activeIndex + 1;
|
|
64
|
+
if (nextIndex < slides.length) {
|
|
65
|
+
listRef.current?.scrollToIndex({ index: nextIndex, animated: true });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
onContinue();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isLastSlide = activeIndex === slides.length - 1;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<SafeAreaView style={styles.safeArea}>
|
|
75
|
+
<FlatList
|
|
76
|
+
ref={listRef}
|
|
77
|
+
data={slides}
|
|
78
|
+
horizontal
|
|
79
|
+
pagingEnabled
|
|
80
|
+
bounces={false}
|
|
81
|
+
keyExtractor={(item) => item.id}
|
|
82
|
+
showsHorizontalScrollIndicator={false}
|
|
83
|
+
renderItem={({ item }) => (
|
|
84
|
+
<View style={[styles.slide, { width }]}>
|
|
85
|
+
<Image source={item.image} style={styles.image} resizeMode="contain" />
|
|
86
|
+
<Text style={styles.title}>{item.title}</Text>
|
|
87
|
+
<Text style={styles.description}>{item.description}</Text>
|
|
88
|
+
</View>
|
|
89
|
+
)}
|
|
90
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
91
|
+
viewabilityConfig={viewabilityConfig}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<View style={styles.footer}>
|
|
95
|
+
<View style={styles.dotsRow}>
|
|
96
|
+
{slides.map((slide, index) => (
|
|
97
|
+
<View
|
|
98
|
+
key={slide.id}
|
|
99
|
+
style={[styles.dot, index === activeIndex && styles.dotActive]}
|
|
100
|
+
/>
|
|
101
|
+
))}
|
|
102
|
+
</View>
|
|
103
|
+
|
|
104
|
+
<Pressable style={styles.button} onPress={handleNext}>
|
|
105
|
+
<Text style={styles.buttonLabel}>{isLastSlide ? "Get Started" : "Next"}</Text>
|
|
106
|
+
</Pressable>
|
|
107
|
+
</View>
|
|
108
|
+
</SafeAreaView>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const styles = StyleSheet.create({
|
|
113
|
+
safeArea: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
backgroundColor: "#F7F7F9",
|
|
116
|
+
},
|
|
117
|
+
slide: {
|
|
118
|
+
flex: 1,
|
|
119
|
+
paddingHorizontal: 24,
|
|
120
|
+
justifyContent: "center",
|
|
121
|
+
alignItems: "center",
|
|
122
|
+
},
|
|
123
|
+
image: {
|
|
124
|
+
width: 220,
|
|
125
|
+
height: 220,
|
|
126
|
+
marginBottom: 28,
|
|
127
|
+
},
|
|
128
|
+
title: {
|
|
129
|
+
fontSize: 28,
|
|
130
|
+
fontWeight: "700",
|
|
131
|
+
color: "#111827",
|
|
132
|
+
textAlign: "center",
|
|
133
|
+
marginBottom: 12,
|
|
134
|
+
},
|
|
135
|
+
description: {
|
|
136
|
+
fontSize: 16,
|
|
137
|
+
lineHeight: 24,
|
|
138
|
+
color: "#4B5563",
|
|
139
|
+
textAlign: "center",
|
|
140
|
+
maxWidth: 320,
|
|
141
|
+
},
|
|
142
|
+
footer: {
|
|
143
|
+
paddingHorizontal: 24,
|
|
144
|
+
paddingBottom: 28,
|
|
145
|
+
gap: 20,
|
|
146
|
+
},
|
|
147
|
+
dotsRow: {
|
|
148
|
+
flexDirection: "row",
|
|
149
|
+
justifyContent: "center",
|
|
150
|
+
gap: 8,
|
|
151
|
+
},
|
|
152
|
+
dot: {
|
|
153
|
+
width: 8,
|
|
154
|
+
height: 8,
|
|
155
|
+
borderRadius: 999,
|
|
156
|
+
backgroundColor: "#D1D5DB",
|
|
157
|
+
},
|
|
158
|
+
dotActive: {
|
|
159
|
+
width: 24,
|
|
160
|
+
backgroundColor: "#111827",
|
|
161
|
+
},
|
|
162
|
+
button: {
|
|
163
|
+
height: 52,
|
|
164
|
+
borderRadius: 12,
|
|
165
|
+
backgroundColor: "#111827",
|
|
166
|
+
alignItems: "center",
|
|
167
|
+
justifyContent: "center",
|
|
168
|
+
},
|
|
169
|
+
buttonLabel: {
|
|
170
|
+
color: "#FFFFFF",
|
|
171
|
+
fontSize: 16,
|
|
172
|
+
fontWeight: "600",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 20
|
|
18
|
+
cache: npm
|
|
19
|
+
- run: npm install
|
|
20
|
+
- run: npm run lint
|
|
21
|
+
|
|
22
|
+
test:
|
|
23
|
+
name: Test
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
- uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: 20
|
|
30
|
+
cache: npm
|
|
31
|
+
- run: npm install
|
|
32
|
+
- run: npm test -- --passWithNoTests
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { Component, type ReactNode } from "react";
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
onReset?: () => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type State = {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
16
|
+
constructor(props: Props) {
|
|
17
|
+
super(props);
|
|
18
|
+
this.state = { hasError: false, error: null };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static getDerivedStateFromError(error: Error): State {
|
|
22
|
+
return { hasError: true, error };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
componentDidCatch(error: Error, info: { componentStack: string }) {
|
|
26
|
+
console.error("ErrorBoundary caught:", error, info.componentStack);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
handleReset = () => {
|
|
30
|
+
this.setState({ hasError: false, error: null });
|
|
31
|
+
this.props.onReset?.();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
render() {
|
|
35
|
+
if (this.state.hasError) {
|
|
36
|
+
if (this.props.fallback) return this.props.fallback;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<View style={styles.container}>
|
|
40
|
+
<Text style={styles.title}>Something went wrong</Text>
|
|
41
|
+
<Text style={styles.message}>{this.state.error?.message}</Text>
|
|
42
|
+
<Pressable style={styles.button} onPress={this.handleReset}>
|
|
43
|
+
<Text style={styles.buttonLabel}>Try again</Text>
|
|
44
|
+
</Pressable>
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this.props.children;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const styles = StyleSheet.create({
|
|
54
|
+
container: {
|
|
55
|
+
flex: 1,
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
justifyContent: "center",
|
|
58
|
+
padding: 24,
|
|
59
|
+
},
|
|
60
|
+
title: {
|
|
61
|
+
fontSize: 20,
|
|
62
|
+
fontWeight: "700",
|
|
63
|
+
color: "#111827",
|
|
64
|
+
marginBottom: 8,
|
|
65
|
+
},
|
|
66
|
+
message: {
|
|
67
|
+
fontSize: 14,
|
|
68
|
+
color: "#6B7280",
|
|
69
|
+
textAlign: "center",
|
|
70
|
+
marginBottom: 24,
|
|
71
|
+
},
|
|
72
|
+
button: {
|
|
73
|
+
paddingHorizontal: 24,
|
|
74
|
+
paddingVertical: 12,
|
|
75
|
+
backgroundColor: "#111827",
|
|
76
|
+
borderRadius: 8,
|
|
77
|
+
},
|
|
78
|
+
buttonLabel: {
|
|
79
|
+
color: "#FFFFFF",
|
|
80
|
+
fontSize: 14,
|
|
81
|
+
fontWeight: "600",
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, TextInput, Text, StyleSheet, TextInputProps } from 'react-native';
|
|
3
|
+
import { useField } from 'formik';
|
|
4
|
+
|
|
5
|
+
interface FormikInputProps extends TextInputProps {
|
|
6
|
+
name: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function FormikInput({ name, label, ...props }: FormikInputProps) {
|
|
11
|
+
const [field, meta, helpers] = useField(name);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<View style={styles.container}>
|
|
15
|
+
<Text style={styles.label}>{label}</Text>
|
|
16
|
+
<TextInput
|
|
17
|
+
style={[styles.input, meta.touched && meta.error ? styles.inputError : undefined]}
|
|
18
|
+
value={field.value}
|
|
19
|
+
onChangeText={helpers.setValue}
|
|
20
|
+
onBlur={() => helpers.setTouched(true)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
{meta.touched && meta.error ? (
|
|
24
|
+
<Text style={styles.errorText}>{meta.error}</Text>
|
|
25
|
+
) : null}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const styles = StyleSheet.create({
|
|
31
|
+
container: { marginBottom: 16 },
|
|
32
|
+
label: { fontSize: 14, fontWeight: '500', marginBottom: 6, color: '#374151' },
|
|
33
|
+
input: {
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderColor: '#D1D5DB',
|
|
36
|
+
borderRadius: 8,
|
|
37
|
+
paddingHorizontal: 12,
|
|
38
|
+
paddingVertical: 10,
|
|
39
|
+
fontSize: 16,
|
|
40
|
+
color: '#111827',
|
|
41
|
+
backgroundColor: '#FFFFFF',
|
|
42
|
+
},
|
|
43
|
+
inputError: { borderColor: '#EF4444' },
|
|
44
|
+
errorText: { marginTop: 4, fontSize: 12, color: '#EF4444' },
|
|
45
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
|
|
3
|
+
import { Formik } from 'formik';
|
|
4
|
+
import { FormikInput } from './FormikInput';
|
|
5
|
+
import { loginSchema, type LoginFormValues } from '../../schemas/auth.schema';
|
|
6
|
+
|
|
7
|
+
interface LoginFormProps {
|
|
8
|
+
onSubmit: (values: LoginFormValues) => Promise<void>;
|
|
9
|
+
isLoading?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const initialValues: LoginFormValues = { email: '', password: '' };
|
|
13
|
+
|
|
14
|
+
export function LoginForm({ onSubmit, isLoading = false }: LoginFormProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Formik initialValues={initialValues} validationSchema={loginSchema} onSubmit={onSubmit}>
|
|
17
|
+
{({ handleSubmit }) => (
|
|
18
|
+
<View style={styles.container}>
|
|
19
|
+
<FormikInput
|
|
20
|
+
name="email"
|
|
21
|
+
label="Email"
|
|
22
|
+
autoCapitalize="none"
|
|
23
|
+
keyboardType="email-address"
|
|
24
|
+
autoComplete="email"
|
|
25
|
+
/>
|
|
26
|
+
<FormikInput
|
|
27
|
+
name="password"
|
|
28
|
+
label="Password"
|
|
29
|
+
secureTextEntry
|
|
30
|
+
autoComplete="current-password"
|
|
31
|
+
/>
|
|
32
|
+
<TouchableOpacity
|
|
33
|
+
style={[styles.button, isLoading && styles.buttonDisabled]}
|
|
34
|
+
onPress={() => handleSubmit()}
|
|
35
|
+
disabled={isLoading}
|
|
36
|
+
>
|
|
37
|
+
{isLoading ? (
|
|
38
|
+
<ActivityIndicator color="#FFFFFF" />
|
|
39
|
+
) : (
|
|
40
|
+
<Text style={styles.buttonText}>Sign In</Text>
|
|
41
|
+
)}
|
|
42
|
+
</TouchableOpacity>
|
|
43
|
+
</View>
|
|
44
|
+
)}
|
|
45
|
+
</Formik>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
container: { width: '100%' },
|
|
51
|
+
button: {
|
|
52
|
+
backgroundColor: '#6366F1',
|
|
53
|
+
borderRadius: 8,
|
|
54
|
+
paddingVertical: 14,
|
|
55
|
+
alignItems: 'center',
|
|
56
|
+
marginTop: 8,
|
|
57
|
+
},
|
|
58
|
+
buttonDisabled: { opacity: 0.6 },
|
|
59
|
+
buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
|
|
60
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Yup from 'yup';
|
|
2
|
+
|
|
3
|
+
export const loginSchema = Yup.object({
|
|
4
|
+
email: Yup.string().email('Invalid email').required('Email is required'),
|
|
5
|
+
password: Yup.string().min(8, 'Minimum 8 characters').required('Password is required'),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const registerSchema = Yup.object({
|
|
9
|
+
email: Yup.string().email('Invalid email').required('Email is required'),
|
|
10
|
+
password: Yup.string().min(8, 'Minimum 8 characters').required('Password is required'),
|
|
11
|
+
confirmPassword: Yup.string()
|
|
12
|
+
.oneOf([Yup.ref('password')], 'Passwords must match')
|
|
13
|
+
.required('Please confirm your password'),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type LoginFormValues = Yup.InferType<typeof loginSchema>;
|
|
17
|
+
export type RegisterFormValues = Yup.InferType<typeof registerSchema>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import type en from '../locales/en.json';
|
|
3
|
+
|
|
4
|
+
// Derive the full key union from the English locale so translations are type-safe
|
|
5
|
+
type TranslationKeys = typeof en;
|
|
6
|
+
type DotPrefix<T extends string, K extends string> = `${T}.${K}`;
|
|
7
|
+
|
|
8
|
+
type LeafKeys<T, Prefix extends string = ''> = {
|
|
9
|
+
[K in keyof T]: T[K] extends object
|
|
10
|
+
? LeafKeys<T[K], Prefix extends '' ? Extract<K, string> : DotPrefix<Prefix, Extract<K, string>>>
|
|
11
|
+
: Prefix extends ''
|
|
12
|
+
? Extract<K, string>
|
|
13
|
+
: DotPrefix<Prefix, Extract<K, string>>;
|
|
14
|
+
}[keyof T];
|
|
15
|
+
|
|
16
|
+
export type AppTranslationKey = LeafKeys<TranslationKeys>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Typed wrapper around react-i18next's useTranslation.
|
|
20
|
+
* Keys are inferred from en.json so you get autocompletion + compile-time safety.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const { t } = useAppTranslation();
|
|
24
|
+
* <Text>{t('auth.signIn')}</Text>
|
|
25
|
+
*/
|
|
26
|
+
export function useAppTranslation() {
|
|
27
|
+
return useTranslation();
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import i18n from 'i18next';
|
|
2
|
+
import { initReactI18next } from 'react-i18next';
|
|
3
|
+
import * as RNLocalize from 'react-native-localize';
|
|
4
|
+
|
|
5
|
+
import en from './locales/en.json';
|
|
6
|
+
import es from './locales/es.json';
|
|
7
|
+
|
|
8
|
+
const resources = {
|
|
9
|
+
en: { translation: en },
|
|
10
|
+
es: { translation: es },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const fallbackLocale = 'en';
|
|
14
|
+
const deviceLocales = RNLocalize.getLocales();
|
|
15
|
+
const bestMatch = deviceLocales[0]?.languageCode ?? fallbackLocale;
|
|
16
|
+
|
|
17
|
+
void i18n
|
|
18
|
+
.use(initReactI18next)
|
|
19
|
+
.init({
|
|
20
|
+
resources,
|
|
21
|
+
lng: bestMatch,
|
|
22
|
+
fallbackLng: fallbackLocale,
|
|
23
|
+
interpolation: {
|
|
24
|
+
// React already escapes values
|
|
25
|
+
escapeValue: false,
|
|
26
|
+
},
|
|
27
|
+
compatibilityJSON: 'v4',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export default i18n;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": {
|
|
3
|
+
"title": "Welcome",
|
|
4
|
+
"subtitle": "Get started with your app",
|
|
5
|
+
"getStarted": "Get Started"
|
|
6
|
+
},
|
|
7
|
+
"auth": {
|
|
8
|
+
"signIn": "Sign In",
|
|
9
|
+
"signUp": "Sign Up",
|
|
10
|
+
"signOut": "Sign Out",
|
|
11
|
+
"email": "Email",
|
|
12
|
+
"password": "Password",
|
|
13
|
+
"confirmPassword": "Confirm Password",
|
|
14
|
+
"forgotPassword": "Forgot password?",
|
|
15
|
+
"noAccount": "Don't have an account?",
|
|
16
|
+
"haveAccount": "Already have an account?"
|
|
17
|
+
},
|
|
18
|
+
"common": {
|
|
19
|
+
"loading": "Loading...",
|
|
20
|
+
"error": "Something went wrong",
|
|
21
|
+
"retry": "Try again",
|
|
22
|
+
"cancel": "Cancel",
|
|
23
|
+
"save": "Save",
|
|
24
|
+
"delete": "Delete",
|
|
25
|
+
"confirm": "Confirm"
|
|
26
|
+
},
|
|
27
|
+
"screens": {
|
|
28
|
+
"home": "Home",
|
|
29
|
+
"profile": "Profile",
|
|
30
|
+
"settings": "Settings"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"welcome": {
|
|
3
|
+
"title": "Bienvenido",
|
|
4
|
+
"subtitle": "Empieza con tu aplicación",
|
|
5
|
+
"getStarted": "Comenzar"
|
|
6
|
+
},
|
|
7
|
+
"auth": {
|
|
8
|
+
"signIn": "Iniciar sesión",
|
|
9
|
+
"signUp": "Registrarse",
|
|
10
|
+
"signOut": "Cerrar sesión",
|
|
11
|
+
"email": "Correo electrónico",
|
|
12
|
+
"password": "Contraseña",
|
|
13
|
+
"confirmPassword": "Confirmar contraseña",
|
|
14
|
+
"forgotPassword": "¿Olvidaste tu contraseña?",
|
|
15
|
+
"noAccount": "¿No tienes cuenta?",
|
|
16
|
+
"haveAccount": "¿Ya tienes cuenta?"
|
|
17
|
+
},
|
|
18
|
+
"common": {
|
|
19
|
+
"loading": "Cargando...",
|
|
20
|
+
"error": "Algo salió mal",
|
|
21
|
+
"retry": "Intentar de nuevo",
|
|
22
|
+
"cancel": "Cancelar",
|
|
23
|
+
"save": "Guardar",
|
|
24
|
+
"delete": "Eliminar",
|
|
25
|
+
"confirm": "Confirmar"
|
|
26
|
+
},
|
|
27
|
+
"screens": {
|
|
28
|
+
"home": "Inicio",
|
|
29
|
+
"profile": "Perfil",
|
|
30
|
+
"settings": "Configuración"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
appId: com.rnstarterkit.app
|
|
2
|
+
---
|
|
3
|
+
- launchApp
|
|
4
|
+
- tapOn: "Get Started"
|
|
5
|
+
- tapOn: "Sign In"
|
|
6
|
+
- tapOn:
|
|
7
|
+
id: "email-input"
|
|
8
|
+
- inputText: "test@example.com"
|
|
9
|
+
- tapOn:
|
|
10
|
+
id: "password-input"
|
|
11
|
+
- inputText: "password123"
|
|
12
|
+
- tapOn: "Sign In"
|
|
13
|
+
- assertVisible: "Home"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
appId: com.rnstarterkit.app
|
|
2
|
+
---
|
|
3
|
+
- launchApp
|
|
4
|
+
- tapOn: "Get Started"
|
|
5
|
+
- tapOn: "Sign In"
|
|
6
|
+
- tapOn:
|
|
7
|
+
id: "email-input"
|
|
8
|
+
- inputText: "test@example.com"
|
|
9
|
+
- tapOn:
|
|
10
|
+
id: "password-input"
|
|
11
|
+
- inputText: "password123"
|
|
12
|
+
- tapOn: "Sign In"
|
|
13
|
+
- assertVisible: "Home"
|
|
14
|
+
- tapOn: "Profile"
|
|
15
|
+
- tapOn: "Sign Out"
|
|
16
|
+
- assertVisible: "Welcome"
|