@highbeek/create-rnstarterkit 1.0.2-beta.5 → 1.1.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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/dist/bin/create-rnstarterkit.js +205 -7
  4. package/dist/src/generators/appGenerator.js +2474 -58
  5. package/dist/src/generators/codeGenerator.js +289 -0
  6. package/dist/templates/cli-base/App.tsx +199 -21
  7. package/dist/templates/cli-base/assets/images/icon.png +0 -0
  8. package/dist/templates/cli-base/assets/images/partial-react-logo.png +0 -0
  9. package/dist/templates/cli-base/assets/images/react-logo.png +0 -0
  10. package/dist/templates/cli-base/babel.config.js +1 -0
  11. package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +6 -0
  12. package/dist/templates/cli-base/ios/Podfile +5 -0
  13. package/dist/templates/cli-base/jest.config.js +4 -0
  14. package/dist/templates/cli-base/package.json +7 -4
  15. package/dist/templates/cli-base/tsconfig.json +1 -0
  16. package/dist/templates/expo-base/app/_layout.tsx +7 -18
  17. package/dist/templates/expo-base/app/home.tsx +37 -0
  18. package/dist/templates/expo-base/app/index.tsx +170 -0
  19. package/dist/templates/expo-base/app.json +2 -1
  20. package/dist/templates/expo-base/package.json +7 -3
  21. package/dist/templates/optional/auth-context/components/layout/ScreenLayout.tsx +46 -0
  22. package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +33 -5
  23. package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +4 -3
  24. package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +42 -7
  25. package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +4 -3
  26. package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +42 -7
  27. package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +4 -3
  28. package/dist/templates/optional/auth-context/screens/WelcomeScreen.tsx +174 -0
  29. package/dist/templates/optional/auth-redux/components/layout/ScreenLayout.tsx +46 -0
  30. package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +39 -2
  31. package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +4 -3
  32. package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +43 -8
  33. package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +7 -10
  34. package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +61 -11
  35. package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +6 -9
  36. package/dist/templates/optional/auth-redux/screens/WelcomeScreen.tsx +174 -0
  37. package/dist/templates/optional/auth-zustand/components/layout/ScreenLayout.tsx +46 -0
  38. package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +34 -6
  39. package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +4 -3
  40. package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +42 -7
  41. package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +4 -3
  42. package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +42 -7
  43. package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +4 -3
  44. package/dist/templates/optional/auth-zustand/screens/WelcomeScreen.tsx +174 -0
  45. package/dist/templates/optional/ci/.github/workflows/ci.yml +32 -0
  46. package/dist/templates/optional/error-boundary/components/ErrorBoundary.tsx +83 -0
  47. package/dist/templates/optional/formik/components/formik/FormikInput.tsx +45 -0
  48. package/dist/templates/optional/formik/components/formik/LoginForm.tsx +60 -0
  49. package/dist/templates/optional/formik/schemas/auth.schema.ts +17 -0
  50. package/dist/templates/optional/i18n/src/i18n/hooks/useAppTranslation.ts +28 -0
  51. package/dist/templates/optional/i18n/src/i18n/i18n.ts +30 -0
  52. package/dist/templates/optional/i18n/src/i18n/locales/en.json +32 -0
  53. package/dist/templates/optional/i18n/src/i18n/locales/es.json +32 -0
  54. package/dist/templates/optional/maestro/.maestro/flows/01_welcome.yaml +5 -0
  55. package/dist/templates/optional/maestro-auth/.maestro/flows/01_welcome.yaml +5 -0
  56. package/dist/templates/optional/maestro-auth/.maestro/flows/02_login.yaml +13 -0
  57. package/dist/templates/optional/maestro-auth/.maestro/flows/03_logout.yaml +16 -0
  58. package/dist/templates/optional/mmkv/utils/storage.ts +17 -0
  59. package/dist/templates/optional/react-hook-form/components/rhf/LoginForm.tsx +63 -0
  60. package/dist/templates/optional/react-hook-form/components/rhf/RHFInput.tsx +50 -0
  61. package/dist/templates/optional/react-hook-form/schemas/auth.schema.ts +29 -0
  62. package/dist/templates/optional/react-query/hooks/useAppMutation.ts +16 -0
  63. package/dist/templates/optional/react-query/hooks/useAppQuery.ts +12 -0
  64. package/dist/templates/optional/react-query/services/queryClient.ts +14 -0
  65. package/dist/templates/optional/redux/store/hooks.ts +6 -0
  66. package/dist/templates/optional/redux/store/store.ts +11 -0
  67. package/dist/templates/optional/sentry/src/utils/sentry.ts +24 -0
  68. package/dist/templates/optional/swr/hooks/useSWRFetch.ts +14 -0
  69. package/dist/templates/optional/swr/providers/SWRProvider.tsx +21 -0
  70. package/dist/templates/optional/tsconfig.json +17 -0
  71. package/dist/templates/optional/zustand/store/appStore.ts +13 -0
  72. package/package.json +40 -5
  73. package/dist/templates/expo-base/App.tsx +0 -32
  74. package/dist/templates/expo-base/app/(tabs)/_layout.tsx +0 -35
  75. package/dist/templates/expo-base/app/(tabs)/explore.tsx +0 -112
  76. package/dist/templates/expo-base/app/(tabs)/index.tsx +0 -98
  77. package/dist/templates/expo-base/app/modal.tsx +0 -29
  78. package/dist/templates/expo-base/components/external-link.tsx +0 -25
  79. package/dist/templates/expo-base/components/haptic-tab.tsx +0 -18
  80. package/dist/templates/expo-base/components/hello-wave.tsx +0 -19
  81. package/dist/templates/expo-base/components/parallax-scroll-view.tsx +0 -79
  82. package/dist/templates/expo-base/components/themed-text.tsx +0 -60
  83. package/dist/templates/expo-base/components/themed-view.tsx +0 -14
  84. package/dist/templates/expo-base/components/ui/collapsible.tsx +0 -45
  85. package/dist/templates/expo-base/components/ui/icon-symbol.ios.tsx +0 -32
  86. package/dist/templates/expo-base/components/ui/icon-symbol.tsx +0 -41
  87. package/dist/templates/expo-base/constants/theme.ts +0 -53
  88. package/dist/templates/expo-base/hooks/use-color-scheme.ts +0 -1
  89. package/dist/templates/expo-base/hooks/use-color-scheme.web.ts +0 -21
  90. package/dist/templates/expo-base/hooks/use-theme-color.ts +0 -21
  91. package/dist/templates/expo-base/scripts/reset-project.js +0 -112
  92. package/dist/templates/optional/apiClient/api/client.axios.ts +0 -124
  93. /package/dist/templates/optional/auth-context/api/{authApi.ts → endpoints/auth.ts} +0 -0
  94. /package/dist/templates/optional/auth-redux/api/{authApi.ts → endpoints/auth.ts} +0 -0
  95. /package/dist/templates/optional/auth-zustand/api/{authApi.ts → endpoints/auth.ts} +0 -0
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { StyleProp, StyleSheet, ViewStyle } from "react-native";
3
+ import { Edge, SafeAreaView } from "react-native-safe-area-context";
4
+
5
+ type ScreenLayoutProps = {
6
+ children: React.ReactNode;
7
+ padded?: boolean;
8
+ centered?: boolean;
9
+ edges?: Edge[];
10
+ style?: StyleProp<ViewStyle>;
11
+ };
12
+
13
+ export default function ScreenLayout({
14
+ children,
15
+ padded = true,
16
+ centered = false,
17
+ edges = ["top", "left", "right"],
18
+ style,
19
+ }: ScreenLayoutProps) {
20
+ return (
21
+ <SafeAreaView
22
+ edges={edges}
23
+ style={[
24
+ styles.container,
25
+ padded && styles.padded,
26
+ centered && styles.centered,
27
+ style,
28
+ ]}
29
+ >
30
+ {children}
31
+ </SafeAreaView>
32
+ );
33
+ }
34
+
35
+ const styles = StyleSheet.create({
36
+ container: {
37
+ flex: 1,
38
+ },
39
+ padded: {
40
+ padding: 20,
41
+ },
42
+ centered: {
43
+ alignItems: "center",
44
+ justifyContent: "center",
45
+ },
46
+ });
@@ -1,30 +1,58 @@
1
- import React, { useEffect } from "react";
2
- import { ActivityIndicator, View } from "react-native";
1
+ import React, { useEffect, useState } from "react";
2
+ import { ActivityIndicator } from "react-native";
3
+ import AsyncStorage from "@react-native-async-storage/async-storage";
3
4
  import { createNativeStackNavigator } from "@react-navigation/native-stack";
5
+ import ScreenLayout from "../components/layout/ScreenLayout";
4
6
  import LoginScreen from "../screens/LoginScreen";
5
7
  import RegisterScreen from "../screens/RegisterScreen";
6
- import BottomTabs from "./BottomTabs";
8
+ import WelcomeScreen from "../screens/WelcomeScreen";
7
9
  import { useAuthStore } from "../store/authStore";
10
+ import BottomTabs from "./BottomTabs";
8
11
 
9
12
  const Stack = createNativeStackNavigator();
13
+ const WELCOME_SEEN_KEY = "@rnskit_has_seen_welcome";
10
14
 
11
15
  export default function ProtectedStack() {
12
16
  const token = useAuthStore((state) => state.token);
13
17
  const isHydrated = useAuthStore((state) => state.isHydrated);
14
18
  const hydrate = useAuthStore((state) => state.hydrate);
19
+ const [showWelcome, setShowWelcome] = useState(false);
20
+ const [isCheckingWelcome, setIsCheckingWelcome] = useState(true);
15
21
 
16
22
  useEffect(() => {
17
23
  void hydrate();
18
24
  }, [hydrate]);
19
25
 
20
- if (!isHydrated) {
26
+ useEffect(() => {
27
+ const readWelcomeState = async () => {
28
+ try {
29
+ const seen = await AsyncStorage.getItem(WELCOME_SEEN_KEY);
30
+ setShowWelcome(seen !== "true");
31
+ } finally {
32
+ setIsCheckingWelcome(false);
33
+ }
34
+ };
35
+
36
+ void readWelcomeState();
37
+ }, []);
38
+
39
+ const handleWelcomeContinue = async () => {
40
+ await AsyncStorage.setItem(WELCOME_SEEN_KEY, "true");
41
+ setShowWelcome(false);
42
+ };
43
+
44
+ if (!isHydrated || isCheckingWelcome) {
21
45
  return (
22
- <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
46
+ <ScreenLayout centered padded={false}>
23
47
  <ActivityIndicator />
24
- </View>
48
+ </ScreenLayout>
25
49
  );
26
50
  }
27
51
 
52
+ if (showWelcome) {
53
+ return <WelcomeScreen onContinue={() => void handleWelcomeContinue()} />;
54
+ }
55
+
28
56
  return (
29
57
  <Stack.Navigator>
30
58
  {token ? (
@@ -1,14 +1,15 @@
1
1
  import React from "react";
2
- import { View, Text, Button } from "react-native";
2
+ import { Text, Button } from "react-native";
3
+ import ScreenLayout from "../components/layout/ScreenLayout";
3
4
  import { useAuthStore } from "../store/authStore";
4
5
 
5
6
  export default function HomeScreen() {
6
7
  const logout = useAuthStore((state) => state.logout);
7
8
 
8
9
  return (
9
- <View style={{ padding: 20 }}>
10
+ <ScreenLayout>
10
11
  <Text>Welcome Home!</Text>
11
12
  <Button title="Logout" onPress={() => void logout()} />
12
- </View>
13
+ </ScreenLayout>
13
14
  );
14
15
  }
@@ -1,28 +1,63 @@
1
1
  import React, { useState } from "react";
2
- import { View, TextInput, Button } from "react-native";
3
- import { loginApi } from "../api/authApi";
2
+ import { TextInput, Button, Text } from "react-native";
3
+ import ScreenLayout from "../components/layout/ScreenLayout";
4
+ import { loginApi } from "../api";
4
5
  import { useAuthStore } from "../store/authStore";
5
6
 
6
7
  export default function LoginScreen() {
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 handleLogin = async () => {
12
- const token = await loginApi(email, password);
13
- await login(token);
14
+ try {
15
+ setError("");
16
+ const token = await loginApi(email, password);
17
+ await login(token);
18
+ } catch (err) {
19
+ setError(err instanceof Error ? err.message : "Login failed");
20
+ }
14
21
  };
15
22
 
16
23
  return (
17
- <View style={{ padding: 20 }}>
18
- <TextInput placeholder="Email" value={email} onChangeText={setEmail} />
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="Login" onPress={() => void handleLogin()} />
26
- </View>
61
+ </ScreenLayout>
27
62
  );
28
63
  }
@@ -1,10 +1,11 @@
1
1
  import React from "react";
2
- import { View, Text } from "react-native";
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
- <View style={{ padding: 20 }}>
7
+ <ScreenLayout>
7
8
  <Text>This is the Profile Screen</Text>
8
- </View>
9
+ </ScreenLayout>
9
10
  );
10
11
  }
@@ -1,28 +1,63 @@
1
1
  import React, { useState } from "react";
2
- import { View, TextInput, Button } from "react-native";
3
- import { registerApi } from "../api/authApi";
2
+ import { TextInput, Button, Text } from "react-native";
3
+ import ScreenLayout from "../components/layout/ScreenLayout";
4
+ import { registerApi } from "../api";
4
5
  import { useAuthStore } from "../store/authStore";
5
6
 
6
7
  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
- const token = await registerApi(email, password);
13
- await login(token);
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
- <View style={{ padding: 20 }}>
18
- <TextInput placeholder="Email" value={email} onChangeText={setEmail} />
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
- </View>
61
+ </ScreenLayout>
27
62
  );
28
63
  }
@@ -1,10 +1,11 @@
1
1
  import React from "react";
2
- import { View, Text } from "react-native";
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
- <View style={{ padding: 20 }}>
7
+ <ScreenLayout>
7
8
  <Text>This is the Settings Screen</Text>
8
- </View>
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
+ });