@highbeek/create-rnstarterkit 1.0.2-beta.1 → 1.0.2-beta.11

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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +254 -0
  3. package/dist/bin/create-rnstarterkit.js +21 -1
  4. package/dist/src/generators/appGenerator.js +2086 -67
  5. package/dist/templates/cli-base/.bundle/config +2 -0
  6. package/dist/templates/cli-base/.eslintrc.js +4 -0
  7. package/dist/templates/cli-base/.prettierrc.js +5 -0
  8. package/dist/templates/cli-base/.watchmanconfig +1 -0
  9. package/dist/templates/cli-base/App.tsx +223 -0
  10. package/dist/templates/cli-base/Gemfile +16 -0
  11. package/dist/templates/cli-base/Gemfile.lock +169 -0
  12. package/dist/templates/cli-base/README.md +97 -0
  13. package/dist/templates/cli-base/__tests__/App.test.tsx +13 -0
  14. package/dist/templates/cli-base/android/app/build.gradle +119 -0
  15. package/dist/templates/cli-base/android/app/debug.keystore +0 -0
  16. package/dist/templates/cli-base/android/app/proguard-rules.pro +10 -0
  17. package/dist/templates/cli-base/android/app/src/main/AndroidManifest.xml +27 -0
  18. package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainActivity.kt +22 -0
  19. package/dist/templates/cli-base/android/app/src/main/java/com/baseapp/MainApplication.kt +27 -0
  20. package/dist/templates/cli-base/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  21. package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  22. package/dist/templates/cli-base/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  23. package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  24. package/dist/templates/cli-base/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  25. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  26. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  27. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  28. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  29. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  30. package/dist/templates/cli-base/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  31. package/dist/templates/cli-base/android/app/src/main/res/values/strings.xml +3 -0
  32. package/dist/templates/cli-base/android/app/src/main/res/values/styles.xml +9 -0
  33. package/dist/templates/cli-base/android/build.gradle +21 -0
  34. package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  35. package/dist/templates/cli-base/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  36. package/dist/templates/cli-base/android/gradle.properties +44 -0
  37. package/dist/templates/cli-base/android/gradlew +251 -0
  38. package/dist/templates/cli-base/android/gradlew.bat +99 -0
  39. package/dist/templates/cli-base/android/settings.gradle +6 -0
  40. package/dist/templates/cli-base/app.json +4 -0
  41. package/dist/templates/cli-base/assets/images/icon.png +0 -0
  42. package/dist/templates/cli-base/assets/images/partial-react-logo.png +0 -0
  43. package/dist/templates/cli-base/assets/images/react-logo.png +0 -0
  44. package/dist/templates/cli-base/babel.config.js +3 -0
  45. package/dist/templates/cli-base/index.js +9 -0
  46. package/dist/templates/cli-base/ios/.xcode.env +11 -0
  47. package/dist/templates/cli-base/ios/BaseApp/AppDelegate.swift +48 -0
  48. package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  49. package/dist/templates/cli-base/ios/BaseApp/Images.xcassets/Contents.json +6 -0
  50. package/dist/templates/cli-base/ios/BaseApp/Info.plist +60 -0
  51. package/dist/templates/cli-base/ios/BaseApp/LaunchScreen.storyboard +47 -0
  52. package/dist/templates/cli-base/ios/BaseApp/PrivacyInfo.xcprivacy +37 -0
  53. package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +494 -0
  54. package/dist/templates/cli-base/ios/BaseApp.xcodeproj/xcshareddata/xcschemes/BaseApp.xcscheme +88 -0
  55. package/dist/templates/cli-base/ios/BaseApp.xcworkspace/contents.xcworkspacedata +10 -0
  56. package/dist/templates/cli-base/ios/Podfile +34 -0
  57. package/dist/templates/cli-base/ios/Podfile.lock +2165 -0
  58. package/dist/templates/cli-base/jest.config.js +7 -0
  59. package/dist/templates/cli-base/metro.config.js +11 -0
  60. package/dist/templates/cli-base/package-lock.json +11859 -0
  61. package/dist/templates/cli-base/package.json +43 -0
  62. package/dist/templates/cli-base/tsconfig.json +9 -0
  63. package/dist/templates/expo-base/.vscode/extensions.json +1 -0
  64. package/dist/templates/expo-base/.vscode/settings.json +7 -0
  65. package/dist/templates/expo-base/README.md +50 -0
  66. package/dist/templates/expo-base/app/_layout.tsx +12 -0
  67. package/dist/templates/expo-base/app/index.tsx +168 -0
  68. package/dist/templates/expo-base/app.json +48 -0
  69. package/dist/templates/expo-base/assets/images/android-icon-background.png +0 -0
  70. package/dist/templates/expo-base/assets/images/android-icon-foreground.png +0 -0
  71. package/dist/templates/expo-base/assets/images/android-icon-monochrome.png +0 -0
  72. package/dist/templates/expo-base/assets/images/favicon.png +0 -0
  73. package/dist/templates/expo-base/assets/images/icon.png +0 -0
  74. package/dist/templates/expo-base/assets/images/partial-react-logo.png +0 -0
  75. package/dist/templates/expo-base/assets/images/react-logo.png +0 -0
  76. package/dist/templates/expo-base/assets/images/react-logo@2x.png +0 -0
  77. package/dist/templates/expo-base/assets/images/react-logo@3x.png +0 -0
  78. package/dist/templates/expo-base/assets/images/splash-icon.png +0 -0
  79. package/dist/templates/expo-base/eslint.config.js +10 -0
  80. package/dist/templates/expo-base/package-lock.json +12916 -0
  81. package/dist/templates/expo-base/package.json +49 -0
  82. package/dist/templates/expo-base/tsconfig.json +17 -0
  83. package/dist/templates/optional/apiClient/api/client.ts +142 -0
  84. package/dist/templates/optional/apiClient/api/index.ts +1 -0
  85. package/dist/templates/optional/apiClient/config/env.cli.ts +5 -0
  86. package/dist/templates/optional/apiClient/config/env.expo.ts +4 -0
  87. package/dist/templates/optional/apiClient/config/env.ts +1 -0
  88. package/dist/templates/optional/apiClient/env.d.ts +3 -0
  89. package/dist/templates/optional/auth-context/api/endpoints/auth.ts +14 -0
  90. package/dist/templates/optional/auth-context/components/layout/ScreenLayout.tsx +46 -0
  91. package/dist/templates/optional/auth-context/context/AuthContext.tsx +47 -0
  92. package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +66 -0
  93. package/dist/templates/optional/auth-context/navigation/cli/BottomTabs.tsx +17 -0
  94. package/dist/templates/optional/auth-context/navigation/expo/BottomTabs.tsx +29 -0
  95. package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +15 -0
  96. package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +63 -0
  97. package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +11 -0
  98. package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +63 -0
  99. package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +11 -0
  100. package/dist/templates/optional/auth-context/screens/WelcomeScreen.tsx +174 -0
  101. package/dist/templates/optional/auth-context/utils/storage.ts +13 -0
  102. package/dist/templates/optional/auth-redux/api/endpoints/auth.ts +14 -0
  103. package/dist/templates/optional/auth-redux/components/layout/ScreenLayout.tsx +46 -0
  104. package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +67 -0
  105. package/dist/templates/optional/auth-redux/navigation/cli/BottomTabs.tsx +17 -0
  106. package/dist/templates/optional/auth-redux/navigation/expo/BottomTabs.tsx +31 -0
  107. package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +16 -0
  108. package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +64 -0
  109. package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +11 -0
  110. package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +64 -0
  111. package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +11 -0
  112. package/dist/templates/optional/auth-redux/screens/WelcomeScreen.tsx +174 -0
  113. package/dist/templates/optional/auth-redux/store/authSlice.ts +25 -0
  114. package/dist/templates/optional/auth-redux/store/store.ts +11 -0
  115. package/dist/templates/optional/auth-zustand/api/endpoints/auth.ts +14 -0
  116. package/dist/templates/optional/auth-zustand/components/layout/ScreenLayout.tsx +46 -0
  117. package/dist/templates/optional/auth-zustand/navigation/BottomTabs.tsx +1 -0
  118. package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +72 -0
  119. package/dist/templates/optional/auth-zustand/navigation/cli/BottomTabs.tsx +17 -0
  120. package/dist/templates/optional/auth-zustand/navigation/expo/BottomTabs.tsx +31 -0
  121. package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +15 -0
  122. package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +63 -0
  123. package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +11 -0
  124. package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +63 -0
  125. package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +11 -0
  126. package/dist/templates/optional/auth-zustand/screens/WelcomeScreen.tsx +174 -0
  127. package/dist/templates/optional/auth-zustand/store/authStore.ts +30 -0
  128. package/dist/templates/optional/auth-zustand/utils/storage.ts +13 -0
  129. package/dist/templates/optional/ci/.github/workflows/ci.yml +32 -0
  130. package/dist/templates/optional/error-boundary/components/ErrorBoundary.tsx +83 -0
  131. package/dist/templates/optional/formik/components/formik/FormikInput.tsx +45 -0
  132. package/dist/templates/optional/formik/components/formik/LoginForm.tsx +60 -0
  133. package/dist/templates/optional/formik/schemas/auth.schema.ts +17 -0
  134. package/dist/templates/optional/mmkv/utils/storage.ts +17 -0
  135. package/dist/templates/optional/react-hook-form/components/rhf/LoginForm.tsx +63 -0
  136. package/dist/templates/optional/react-hook-form/components/rhf/RHFInput.tsx +50 -0
  137. package/dist/templates/optional/react-hook-form/schemas/auth.schema.ts +29 -0
  138. package/dist/templates/optional/react-query/hooks/useAppMutation.ts +16 -0
  139. package/dist/templates/optional/react-query/hooks/useAppQuery.ts +12 -0
  140. package/dist/templates/optional/react-query/services/queryClient.ts +14 -0
  141. package/dist/templates/optional/redux/store/hooks.ts +6 -0
  142. package/dist/templates/optional/redux/store/store.ts +11 -0
  143. package/dist/templates/optional/swr/hooks/useSWRFetch.ts +14 -0
  144. package/dist/templates/optional/swr/providers/SWRProvider.tsx +21 -0
  145. package/dist/templates/optional/tsconfig.json +17 -0
  146. package/dist/templates/optional/zustand/store/appStore.ts +13 -0
  147. package/package.json +2 -2
@@ -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,30 @@
1
+ import { create } from "zustand";
2
+ import { getToken, removeToken, storeToken } from "../utils/storage";
3
+
4
+ type AuthState = {
5
+ token: string | null;
6
+ isHydrated: boolean;
7
+ hydrate: () => Promise<void>;
8
+ login: (token: string) => Promise<void>;
9
+ logout: () => Promise<void>;
10
+ };
11
+
12
+ export const useAuthStore = create<AuthState>((set) => ({
13
+ token: null,
14
+ isHydrated: false,
15
+
16
+ hydrate: async () => {
17
+ const token = await getToken();
18
+ set({ token, isHydrated: true });
19
+ },
20
+
21
+ login: async (token: string) => {
22
+ await storeToken(token);
23
+ set({ token });
24
+ },
25
+
26
+ logout: async () => {
27
+ await removeToken();
28
+ set({ token: null });
29
+ },
30
+ }));
@@ -0,0 +1,13 @@
1
+ import AsyncStorage from "@react-native-async-storage/async-storage";
2
+
3
+ export const storeToken = async (token: string) => {
4
+ await AsyncStorage.setItem("@token", token);
5
+ };
6
+
7
+ export const getToken = async () => {
8
+ return await AsyncStorage.getItem("@token");
9
+ };
10
+
11
+ export const removeToken = async () => {
12
+ await AsyncStorage.removeItem("@token");
13
+ };
@@ -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,17 @@
1
+ import { MMKV } from "react-native-mmkv";
2
+
3
+ const storage = new MMKV();
4
+
5
+ export const storeToken = (token: string): void => {
6
+ storage.set("@token", token);
7
+ };
8
+
9
+ export const getToken = (): string | null => {
10
+ return storage.getString("@token") ?? null;
11
+ };
12
+
13
+ export const removeToken = (): void => {
14
+ storage.delete("@token");
15
+ };
16
+
17
+ export { storage as mmkvStorage };
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { View, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native';
3
+ import { useForm, FormProvider } from 'react-hook-form';
4
+ import { RHFInput } from './RHFInput';
5
+ import { loginRules, type LoginFormValues } from '../../schemas/auth.schema';
6
+
7
+ interface LoginFormProps {
8
+ onSubmit: (values: LoginFormValues) => Promise<void>;
9
+ isLoading?: boolean;
10
+ }
11
+
12
+ export function LoginForm({ onSubmit, isLoading = false }: LoginFormProps) {
13
+ const methods = useForm<LoginFormValues>({
14
+ defaultValues: { email: '', password: '' },
15
+ mode: 'onBlur',
16
+ });
17
+
18
+ return (
19
+ <FormProvider {...methods}>
20
+ <View style={styles.container}>
21
+ <RHFInput
22
+ name="email"
23
+ label="Email"
24
+ rules={loginRules.email}
25
+ autoCapitalize="none"
26
+ keyboardType="email-address"
27
+ autoComplete="email"
28
+ />
29
+ <RHFInput
30
+ name="password"
31
+ label="Password"
32
+ rules={loginRules.password}
33
+ secureTextEntry
34
+ autoComplete="current-password"
35
+ />
36
+ <TouchableOpacity
37
+ style={[styles.button, isLoading && styles.buttonDisabled]}
38
+ onPress={methods.handleSubmit(onSubmit)}
39
+ disabled={isLoading}
40
+ >
41
+ {isLoading ? (
42
+ <ActivityIndicator color="#FFFFFF" />
43
+ ) : (
44
+ <Text style={styles.buttonText}>Sign In</Text>
45
+ )}
46
+ </TouchableOpacity>
47
+ </View>
48
+ </FormProvider>
49
+ );
50
+ }
51
+
52
+ const styles = StyleSheet.create({
53
+ container: { width: '100%' },
54
+ button: {
55
+ backgroundColor: '#6366F1',
56
+ borderRadius: 8,
57
+ paddingVertical: 14,
58
+ alignItems: 'center',
59
+ marginTop: 8,
60
+ },
61
+ buttonDisabled: { opacity: 0.6 },
62
+ buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
63
+ });
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { View, TextInput, Text, StyleSheet, TextInputProps } from 'react-native';
3
+ import { Controller, useFormContext } from 'react-hook-form';
4
+
5
+ interface RHFInputProps extends TextInputProps {
6
+ name: string;
7
+ label: string;
8
+ }
9
+
10
+ export function RHFInput({ name, label, ...props }: RHFInputProps) {
11
+ const { control, formState: { errors } } = useFormContext();
12
+ const error = errors[name]?.message as string | undefined;
13
+
14
+ return (
15
+ <View style={styles.container}>
16
+ <Text style={styles.label}>{label}</Text>
17
+ <Controller
18
+ control={control}
19
+ name={name}
20
+ render={({ field: { onChange, onBlur, value } }) => (
21
+ <TextInput
22
+ style={[styles.input, error ? styles.inputError : undefined]}
23
+ value={value}
24
+ onChangeText={onChange}
25
+ onBlur={onBlur}
26
+ {...props}
27
+ />
28
+ )}
29
+ />
30
+ {error ? <Text style={styles.errorText}>{error}</Text> : null}
31
+ </View>
32
+ );
33
+ }
34
+
35
+ const styles = StyleSheet.create({
36
+ container: { marginBottom: 16 },
37
+ label: { fontSize: 14, fontWeight: '500', marginBottom: 6, color: '#374151' },
38
+ input: {
39
+ borderWidth: 1,
40
+ borderColor: '#D1D5DB',
41
+ borderRadius: 8,
42
+ paddingHorizontal: 12,
43
+ paddingVertical: 10,
44
+ fontSize: 16,
45
+ color: '#111827',
46
+ backgroundColor: '#FFFFFF',
47
+ },
48
+ inputError: { borderColor: '#EF4444' },
49
+ errorText: { marginTop: 4, fontSize: 12, color: '#EF4444' },
50
+ });
@@ -0,0 +1,29 @@
1
+ export const loginRules = {
2
+ email: {
3
+ required: 'Email is required',
4
+ pattern: { value: /\S+@\S+\.\S+/, message: 'Invalid email address' },
5
+ },
6
+ password: {
7
+ required: 'Password is required',
8
+ minLength: { value: 8, message: 'Minimum 8 characters' },
9
+ },
10
+ } as const;
11
+
12
+ export const registerRules = {
13
+ email: loginRules.email,
14
+ password: loginRules.password,
15
+ confirmPassword: {
16
+ required: 'Please confirm your password',
17
+ },
18
+ } as const;
19
+
20
+ export interface LoginFormValues {
21
+ email: string;
22
+ password: string;
23
+ }
24
+
25
+ export interface RegisterFormValues {
26
+ email: string;
27
+ password: string;
28
+ confirmPassword: string;
29
+ }
@@ -0,0 +1,16 @@
1
+ import {
2
+ useMutation,
3
+ type UseMutationOptions,
4
+ type UseMutationResult,
5
+ } from "@tanstack/react-query";
6
+
7
+ export function useAppMutation<
8
+ TData = unknown,
9
+ TError = Error,
10
+ TVariables = void,
11
+ TContext = unknown,
12
+ >(
13
+ options: UseMutationOptions<TData, TError, TVariables, TContext>,
14
+ ): UseMutationResult<TData, TError, TVariables, TContext> {
15
+ return useMutation<TData, TError, TVariables, TContext>(options);
16
+ }
@@ -0,0 +1,12 @@
1
+ import {
2
+ useQuery,
3
+ type UseQueryOptions,
4
+ type QueryKey,
5
+ type UseQueryResult,
6
+ } from "@tanstack/react-query";
7
+
8
+ export function useAppQuery<TData, TError = Error>(
9
+ options: UseQueryOptions<TData, TError, TData, QueryKey>,
10
+ ): UseQueryResult<TData, TError> {
11
+ return useQuery<TData, TError, TData, QueryKey>(options);
12
+ }
@@ -0,0 +1,14 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+
3
+ export const queryClient = new QueryClient({
4
+ defaultOptions: {
5
+ queries: {
6
+ retry: 2,
7
+ staleTime: 1000 * 60 * 5, // 5 minutes
8
+ gcTime: 1000 * 60 * 10, // 10 minutes
9
+ },
10
+ mutations: {
11
+ retry: 0,
12
+ },
13
+ },
14
+ });
@@ -0,0 +1,6 @@
1
+ import { useDispatch, useSelector } from "react-redux";
2
+ import type { TypedUseSelectorHook } from "react-redux";
3
+ import type { RootState, AppDispatch } from "./store";
4
+
5
+ export const useAppDispatch = () => useDispatch<AppDispatch>();
6
+ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
@@ -0,0 +1,11 @@
1
+ import { configureStore } from "@reduxjs/toolkit";
2
+
3
+ export const store = configureStore({
4
+ reducer: {
5
+ // Add your slice reducers here
6
+ // example: counter: counterReducer,
7
+ },
8
+ });
9
+
10
+ export type RootState = ReturnType<typeof store.getState>;
11
+ export type AppDispatch = typeof store.dispatch;