@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.
Files changed (109) hide show
  1. package/.env.example +18 -0
  2. package/.eslintrc.js +55 -0
  3. package/.github/workflows/ci.yml +184 -0
  4. package/.github/workflows/eas-build.yml +55 -0
  5. package/.github/workflows/eas-update.yml +50 -0
  6. package/.gitignore +62 -0
  7. package/.prettierrc +11 -0
  8. package/.storybook/main.ts +28 -0
  9. package/.storybook/preview.tsx +30 -0
  10. package/CHANGELOG.md +106 -0
  11. package/CONTRIBUTING.md +377 -0
  12. package/README.md +399 -0
  13. package/__tests__/components/Button.test.tsx +74 -0
  14. package/__tests__/hooks/useAuth.test.tsx +499 -0
  15. package/__tests__/services/api.test.ts +535 -0
  16. package/__tests__/utils/cn.test.ts +39 -0
  17. package/app/(auth)/_layout.tsx +36 -0
  18. package/app/(auth)/home.tsx +117 -0
  19. package/app/(auth)/profile.tsx +152 -0
  20. package/app/(auth)/settings.tsx +147 -0
  21. package/app/(public)/_layout.tsx +21 -0
  22. package/app/(public)/forgot-password.tsx +127 -0
  23. package/app/(public)/login.tsx +120 -0
  24. package/app/(public)/onboarding.tsx +5 -0
  25. package/app/(public)/register.tsx +139 -0
  26. package/app/_layout.tsx +97 -0
  27. package/app/index.tsx +21 -0
  28. package/app.config.ts +72 -0
  29. package/assets/images/.gitkeep +7 -0
  30. package/assets/images/adaptive-icon.png +0 -0
  31. package/assets/images/favicon.png +0 -0
  32. package/assets/images/icon.png +0 -0
  33. package/assets/images/notification-icon.png +0 -0
  34. package/assets/images/splash.png +0 -0
  35. package/babel.config.js +10 -0
  36. package/components/ErrorBoundary.tsx +169 -0
  37. package/components/forms/FormInput.tsx +78 -0
  38. package/components/forms/index.ts +1 -0
  39. package/components/onboarding/OnboardingScreen.tsx +370 -0
  40. package/components/onboarding/index.ts +2 -0
  41. package/components/ui/AnimatedButton.tsx +156 -0
  42. package/components/ui/AnimatedCard.tsx +108 -0
  43. package/components/ui/Avatar.tsx +316 -0
  44. package/components/ui/Badge.tsx +416 -0
  45. package/components/ui/BottomSheet.tsx +307 -0
  46. package/components/ui/Button.stories.tsx +115 -0
  47. package/components/ui/Button.tsx +104 -0
  48. package/components/ui/Card.stories.tsx +84 -0
  49. package/components/ui/Card.tsx +32 -0
  50. package/components/ui/Checkbox.tsx +261 -0
  51. package/components/ui/Input.stories.tsx +106 -0
  52. package/components/ui/Input.tsx +117 -0
  53. package/components/ui/Modal.tsx +98 -0
  54. package/components/ui/OptimizedImage.tsx +369 -0
  55. package/components/ui/Select.tsx +240 -0
  56. package/components/ui/Skeleton.tsx +180 -0
  57. package/components/ui/index.ts +18 -0
  58. package/constants/config.ts +54 -0
  59. package/docs/adr/001-state-management.md +79 -0
  60. package/docs/adr/002-styling-approach.md +130 -0
  61. package/docs/adr/003-data-fetching.md +155 -0
  62. package/docs/adr/004-auth-adapter-pattern.md +144 -0
  63. package/docs/adr/README.md +78 -0
  64. package/eas.json +47 -0
  65. package/global.css +10 -0
  66. package/hooks/index.ts +25 -0
  67. package/hooks/useApi.ts +236 -0
  68. package/hooks/useAuth.tsx +290 -0
  69. package/hooks/useBiometrics.ts +295 -0
  70. package/hooks/useDeepLinking.ts +256 -0
  71. package/hooks/useNotifications.ts +138 -0
  72. package/hooks/useOffline.ts +69 -0
  73. package/hooks/usePerformance.ts +434 -0
  74. package/hooks/useTheme.tsx +85 -0
  75. package/hooks/useUpdates.ts +358 -0
  76. package/i18n/index.ts +77 -0
  77. package/i18n/locales/en.json +101 -0
  78. package/i18n/locales/fr.json +101 -0
  79. package/jest.config.js +32 -0
  80. package/maestro/README.md +113 -0
  81. package/maestro/config.yaml +35 -0
  82. package/maestro/flows/login.yaml +62 -0
  83. package/maestro/flows/navigation.yaml +68 -0
  84. package/maestro/flows/offline.yaml +60 -0
  85. package/maestro/flows/register.yaml +94 -0
  86. package/metro.config.js +6 -0
  87. package/nativewind-env.d.ts +1 -0
  88. package/package.json +170 -0
  89. package/scripts/init.ps1 +162 -0
  90. package/scripts/init.sh +174 -0
  91. package/services/analytics.ts +428 -0
  92. package/services/api.ts +340 -0
  93. package/services/authAdapter.ts +333 -0
  94. package/services/index.ts +22 -0
  95. package/services/queryClient.ts +97 -0
  96. package/services/sentry.ts +131 -0
  97. package/services/storage.ts +82 -0
  98. package/stores/appStore.ts +54 -0
  99. package/stores/index.ts +2 -0
  100. package/stores/notificationStore.ts +40 -0
  101. package/tailwind.config.js +47 -0
  102. package/tsconfig.json +26 -0
  103. package/types/index.ts +42 -0
  104. package/types/user.ts +63 -0
  105. package/utils/accessibility.ts +446 -0
  106. package/utils/cn.ts +14 -0
  107. package/utils/index.ts +43 -0
  108. package/utils/toast.ts +113 -0
  109. package/utils/validation.ts +67 -0
@@ -0,0 +1,5 @@
1
+ import { OnboardingScreen } from "@/components/onboarding";
2
+
3
+ export default function Onboarding() {
4
+ return <OnboardingScreen />;
5
+ }
@@ -0,0 +1,139 @@
1
+ import { View, Text, Pressable, KeyboardAvoidingView, Platform, ScrollView } 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 { registerSchema, RegisterFormData } from "@/utils/validation";
12
+
13
+ export default function RegisterScreen() {
14
+ const { signUp } = useAuth();
15
+
16
+ const {
17
+ control,
18
+ handleSubmit,
19
+ formState: { isSubmitting },
20
+ } = useForm<RegisterFormData>({
21
+ resolver: zodResolver(registerSchema),
22
+ defaultValues: {
23
+ name: "",
24
+ email: "",
25
+ password: "",
26
+ confirmPassword: "",
27
+ },
28
+ });
29
+
30
+ const onSubmit = async (data: RegisterFormData) => {
31
+ try {
32
+ await signUp(data.email, data.password, data.name);
33
+ router.replace("/(auth)/home");
34
+ } catch {
35
+ // Error is handled by useAuth with toast
36
+ }
37
+ };
38
+
39
+ return (
40
+ <SafeAreaView className="flex-1 bg-background-light dark:bg-background-dark">
41
+ <KeyboardAvoidingView
42
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
43
+ className="flex-1"
44
+ >
45
+ <ScrollView
46
+ contentContainerStyle={{ flexGrow: 1, justifyContent: "center" }}
47
+ keyboardShouldPersistTaps="handled"
48
+ >
49
+ <View className="flex-1 justify-center px-6 py-8">
50
+ {/* Header */}
51
+ <Animated.View
52
+ entering={FadeInDown.delay(100).springify()}
53
+ className="mb-8"
54
+ >
55
+ <Text className="text-3xl font-bold text-text-light dark:text-text-dark">
56
+ Create account
57
+ </Text>
58
+ <Text className="mt-2 text-muted-light dark:text-muted-dark">
59
+ Sign up to get started
60
+ </Text>
61
+ </Animated.View>
62
+
63
+ {/* Form */}
64
+ <Animated.View
65
+ entering={FadeInDown.delay(200).springify()}
66
+ className="gap-4"
67
+ >
68
+ <FormInput
69
+ name="name"
70
+ control={control}
71
+ label="Name"
72
+ placeholder="Enter your name"
73
+ autoComplete="name"
74
+ leftIcon="person-outline"
75
+ />
76
+
77
+ <FormInput
78
+ name="email"
79
+ control={control}
80
+ label="Email"
81
+ placeholder="Enter your email"
82
+ keyboardType="email-address"
83
+ autoCapitalize="none"
84
+ autoComplete="email"
85
+ leftIcon="mail-outline"
86
+ />
87
+
88
+ <FormInput
89
+ name="password"
90
+ control={control}
91
+ label="Password"
92
+ placeholder="Create a password"
93
+ secureTextEntry
94
+ autoComplete="new-password"
95
+ leftIcon="lock-closed-outline"
96
+ hint="Min 8 chars, 1 uppercase, 1 lowercase, 1 number"
97
+ />
98
+
99
+ <FormInput
100
+ name="confirmPassword"
101
+ control={control}
102
+ label="Confirm Password"
103
+ placeholder="Confirm your password"
104
+ secureTextEntry
105
+ autoComplete="new-password"
106
+ leftIcon="lock-closed-outline"
107
+ />
108
+
109
+ <AnimatedButton
110
+ onPress={handleSubmit(onSubmit)}
111
+ isLoading={isSubmitting}
112
+ className="mt-4"
113
+ >
114
+ Create Account
115
+ </AnimatedButton>
116
+ </Animated.View>
117
+
118
+ {/* Footer */}
119
+ <Animated.View
120
+ entering={FadeInDown.delay(300).springify()}
121
+ className="mt-8 flex-row justify-center"
122
+ >
123
+ <Text className="text-muted-light dark:text-muted-dark">
124
+ Already have an account?{" "}
125
+ </Text>
126
+ <Link href="/(public)/login" asChild>
127
+ <Pressable>
128
+ <Text className="font-semibold text-primary-600 dark:text-primary-400">
129
+ Sign In
130
+ </Text>
131
+ </Pressable>
132
+ </Link>
133
+ </Animated.View>
134
+ </View>
135
+ </ScrollView>
136
+ </KeyboardAvoidingView>
137
+ </SafeAreaView>
138
+ );
139
+ }
@@ -0,0 +1,97 @@
1
+ import "../global.css";
2
+ import { useEffect } from "react";
3
+ import { Stack } from "expo-router";
4
+ import { StatusBar } from "expo-status-bar";
5
+ import * as SplashScreen from "expo-splash-screen";
6
+ import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
7
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
8
+ import { SafeAreaProvider } from "react-native-safe-area-context";
9
+
10
+ import { ErrorBoundary } from "@/components/ErrorBoundary";
11
+ import { AuthProvider } from "@/hooks/useAuth";
12
+ import { ThemeProvider, useTheme } from "@/hooks/useTheme";
13
+ import { useNotifications } from "@/hooks/useNotifications";
14
+ import { useOffline } from "@/hooks/useOffline";
15
+ import {
16
+ queryClient,
17
+ persistOptions,
18
+ setupOnlineManager,
19
+ setupFocusManager,
20
+ } from "@/services/queryClient";
21
+ import { initSentry } from "@/services/sentry";
22
+
23
+ // Initialize Sentry as early as possible
24
+ initSentry();
25
+
26
+ // Prevent splash screen from auto-hiding
27
+ SplashScreen.preventAutoHideAsync();
28
+
29
+ // Setup TanStack Query online/offline management
30
+ setupOnlineManager();
31
+
32
+ function RootLayoutContent() {
33
+ const { isDark, isLoaded } = useTheme();
34
+ const { registerForPushNotifications } = useNotifications();
35
+
36
+ // Track offline status with toast notifications
37
+ useOffline({ showToast: true });
38
+
39
+ useEffect(() => {
40
+ if (isLoaded) {
41
+ SplashScreen.hideAsync();
42
+ }
43
+ }, [isLoaded]);
44
+
45
+ useEffect(() => {
46
+ registerForPushNotifications();
47
+ }, []);
48
+
49
+ // Setup focus manager (refetch on app focus)
50
+ useEffect(() => {
51
+ const cleanup = setupFocusManager();
52
+ return cleanup;
53
+ }, []);
54
+
55
+ if (!isLoaded) {
56
+ return null;
57
+ }
58
+
59
+ return (
60
+ <>
61
+ <Stack
62
+ screenOptions={{
63
+ headerShown: false,
64
+ contentStyle: {
65
+ backgroundColor: isDark ? "#0f172a" : "#ffffff",
66
+ },
67
+ animation: "slide_from_right",
68
+ }}
69
+ >
70
+ <Stack.Screen name="(public)" />
71
+ <Stack.Screen name="(auth)" />
72
+ </Stack>
73
+ <StatusBar style={isDark ? "light" : "dark"} />
74
+ </>
75
+ );
76
+ }
77
+
78
+ export default function RootLayout() {
79
+ return (
80
+ <ErrorBoundary>
81
+ <GestureHandlerRootView style={{ flex: 1 }}>
82
+ <SafeAreaProvider>
83
+ <PersistQueryClientProvider
84
+ client={queryClient}
85
+ persistOptions={persistOptions}
86
+ >
87
+ <ThemeProvider>
88
+ <AuthProvider>
89
+ <RootLayoutContent />
90
+ </AuthProvider>
91
+ </ThemeProvider>
92
+ </PersistQueryClientProvider>
93
+ </SafeAreaProvider>
94
+ </GestureHandlerRootView>
95
+ </ErrorBoundary>
96
+ );
97
+ }
package/app/index.tsx ADDED
@@ -0,0 +1,21 @@
1
+ import { Redirect } from "expo-router";
2
+ import { useAuth } from "@/hooks/useAuth";
3
+ import { View, ActivityIndicator } from "react-native";
4
+
5
+ export default function Index() {
6
+ const { isAuthenticated, isLoading } = useAuth();
7
+
8
+ if (isLoading) {
9
+ return (
10
+ <View className="flex-1 items-center justify-center bg-background-light dark:bg-background-dark">
11
+ <ActivityIndicator size="large" color="#3b82f6" />
12
+ </View>
13
+ );
14
+ }
15
+
16
+ if (isAuthenticated) {
17
+ return <Redirect href="/(auth)/home" />;
18
+ }
19
+
20
+ return <Redirect href="/(public)/login" />;
21
+ }
package/app.config.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { ExpoConfig, ConfigContext } from "expo/config";
2
+
3
+ const IS_DEV = process.env.APP_VARIANT === "development";
4
+ const IS_PREVIEW = process.env.APP_VARIANT === "preview";
5
+
6
+ const getUniqueIdentifier = () => {
7
+ if (IS_DEV) return "com.yourcompany.yourapp.dev";
8
+ if (IS_PREVIEW) return "com.yourcompany.yourapp.preview";
9
+ return "com.yourcompany.yourapp";
10
+ };
11
+
12
+ const getAppName = () => {
13
+ if (IS_DEV) return "YourApp (Dev)";
14
+ if (IS_PREVIEW) return "YourApp (Preview)";
15
+ return "YourApp";
16
+ };
17
+
18
+ export default ({ config }: ConfigContext): ExpoConfig => ({
19
+ ...config,
20
+ name: getAppName(),
21
+ slug: "your-app",
22
+ version: "1.0.0",
23
+ orientation: "portrait",
24
+ icon: "./assets/images/icon.png",
25
+ scheme: "yourapp",
26
+ userInterfaceStyle: "automatic",
27
+ splash: {
28
+ image: "./assets/images/splash.png",
29
+ resizeMode: "contain",
30
+ backgroundColor: "#ffffff",
31
+ },
32
+ assetBundlePatterns: ["**/*"],
33
+ ios: {
34
+ supportsTablet: true,
35
+ bundleIdentifier: getUniqueIdentifier(),
36
+ infoPlist: {
37
+ UIBackgroundModes: ["remote-notification"],
38
+ },
39
+ },
40
+ android: {
41
+ adaptiveIcon: {
42
+ foregroundImage: "./assets/images/adaptive-icon.png",
43
+ backgroundColor: "#ffffff",
44
+ },
45
+ package: getUniqueIdentifier(),
46
+ permissions: ["NOTIFICATIONS"],
47
+ },
48
+ web: {
49
+ bundler: "metro",
50
+ output: "static",
51
+ favicon: "./assets/images/favicon.png",
52
+ },
53
+ plugins: [
54
+ "expo-router",
55
+ "expo-secure-store",
56
+ [
57
+ "expo-notifications",
58
+ {
59
+ icon: "./assets/images/notification-icon.png",
60
+ color: "#ffffff",
61
+ },
62
+ ],
63
+ ],
64
+ experiments: {
65
+ typedRoutes: true,
66
+ },
67
+ extra: {
68
+ eas: {
69
+ projectId: "your-project-id",
70
+ },
71
+ },
72
+ });
@@ -0,0 +1,7 @@
1
+ # Placeholder for app assets
2
+ # Replace these files with your own:
3
+ # - icon.png (1024x1024)
4
+ # - splash.png (1284x2778)
5
+ # - adaptive-icon.png (1024x1024)
6
+ # - favicon.png (48x48)
7
+ # - notification-icon.png (96x96, white on transparent)
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,10 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ return {
4
+ presets: [
5
+ ["babel-preset-expo", { jsxImportSource: "nativewind" }],
6
+ "nativewind/babel",
7
+ ],
8
+ plugins: ["react-native-reanimated/plugin"],
9
+ };
10
+ };
@@ -0,0 +1,169 @@
1
+ import { Component, ErrorInfo, ReactNode } from "react";
2
+ import { View, Text, Pressable, ScrollView } from "react-native";
3
+ import { Ionicons } from "@expo/vector-icons";
4
+ import * as Updates from "expo-updates";
5
+ import { captureException, addBreadcrumb } from "@/services/sentry";
6
+
7
+ interface Props {
8
+ children: ReactNode;
9
+ fallback?: ReactNode;
10
+ }
11
+
12
+ interface State {
13
+ hasError: boolean;
14
+ error: Error | null;
15
+ errorInfo: ErrorInfo | null;
16
+ }
17
+
18
+ /**
19
+ * Global Error Boundary component
20
+ * Catches JavaScript errors anywhere in the child component tree
21
+ * and displays a fallback UI instead of crashing the whole app
22
+ */
23
+ export class ErrorBoundary extends Component<Props, State> {
24
+ constructor(props: Props) {
25
+ super(props);
26
+ this.state = {
27
+ hasError: false,
28
+ error: null,
29
+ errorInfo: null,
30
+ };
31
+ }
32
+
33
+ static getDerivedStateFromError(error: Error): Partial<State> {
34
+ return { hasError: true, error };
35
+ }
36
+
37
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
38
+ this.setState({ errorInfo });
39
+
40
+ // Log to your error reporting service (Sentry, Bugsnag, etc.)
41
+ this.logError(error, errorInfo);
42
+ }
43
+
44
+ private logError(error: Error, errorInfo: ErrorInfo) {
45
+ // Add breadcrumb for context
46
+ addBreadcrumb("error-boundary", "Error caught by ErrorBoundary", {
47
+ componentStack: errorInfo.componentStack?.slice(0, 500),
48
+ });
49
+
50
+ // Send to Sentry
51
+ captureException(error, {
52
+ componentStack: errorInfo.componentStack,
53
+ });
54
+
55
+ // Also log to console in dev
56
+ console.error("ErrorBoundary caught an error:", error);
57
+ console.error("Component stack:", errorInfo.componentStack);
58
+ }
59
+
60
+ private handleRestart = async () => {
61
+ try {
62
+ // Try to reload the app using expo-updates
63
+ if (!__DEV__) {
64
+ await Updates.reloadAsync();
65
+ } else {
66
+ // In dev, just reset the error state
67
+ this.setState({
68
+ hasError: false,
69
+ error: null,
70
+ errorInfo: null,
71
+ });
72
+ }
73
+ } catch (e) {
74
+ // If Updates.reloadAsync fails, reset state
75
+ this.setState({
76
+ hasError: false,
77
+ error: null,
78
+ errorInfo: null,
79
+ });
80
+ }
81
+ };
82
+
83
+ private handleReset = () => {
84
+ this.setState({
85
+ hasError: false,
86
+ error: null,
87
+ errorInfo: null,
88
+ });
89
+ };
90
+
91
+ render() {
92
+ if (this.state.hasError) {
93
+ if (this.props.fallback) {
94
+ return this.props.fallback;
95
+ }
96
+
97
+ return (
98
+ <View className="flex-1 items-center justify-center bg-background-light px-6 dark:bg-background-dark">
99
+ {/* Error Icon */}
100
+ <View className="mb-6 h-24 w-24 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30">
101
+ <Ionicons name="warning-outline" size={48} color="#ef4444" />
102
+ </View>
103
+
104
+ {/* Title */}
105
+ <Text className="mb-2 text-center text-2xl font-bold text-text-light dark:text-text-dark">
106
+ Oops! Something went wrong
107
+ </Text>
108
+
109
+ {/* Description */}
110
+ <Text className="mb-8 text-center text-muted-light dark:text-muted-dark">
111
+ The app ran into a problem and could not continue. We apologize for
112
+ any inconvenience.
113
+ </Text>
114
+
115
+ {/* Error details (dev only) */}
116
+ {__DEV__ && this.state.error && (
117
+ <ScrollView className="mb-6 max-h-32 w-full rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
118
+ <Text className="font-mono text-xs text-red-600 dark:text-red-400">
119
+ {this.state.error.toString()}
120
+ </Text>
121
+ {this.state.errorInfo && (
122
+ <Text className="mt-2 font-mono text-xs text-gray-600 dark:text-gray-400">
123
+ {this.state.errorInfo.componentStack?.slice(0, 500)}...
124
+ </Text>
125
+ )}
126
+ </ScrollView>
127
+ )}
128
+
129
+ {/* Action buttons */}
130
+ <View className="w-full gap-3">
131
+ <Pressable
132
+ onPress={this.handleRestart}
133
+ className="w-full items-center rounded-xl bg-primary-600 py-4"
134
+ >
135
+ <Text className="font-semibold text-white">Restart App</Text>
136
+ </Pressable>
137
+
138
+ <Pressable
139
+ onPress={this.handleReset}
140
+ className="w-full items-center rounded-xl border-2 border-gray-300 py-4 dark:border-gray-600"
141
+ >
142
+ <Text className="font-semibold text-text-light dark:text-text-dark">
143
+ Try Again
144
+ </Text>
145
+ </Pressable>
146
+ </View>
147
+ </View>
148
+ );
149
+ }
150
+
151
+ return this.props.children;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * HOC to wrap any component with error boundary
157
+ */
158
+ export function withErrorBoundary<P extends object>(
159
+ WrappedComponent: React.ComponentType<P>,
160
+ fallback?: ReactNode
161
+ ) {
162
+ return function WithErrorBoundary(props: P) {
163
+ return (
164
+ <ErrorBoundary fallback={fallback}>
165
+ <WrappedComponent {...props} />
166
+ </ErrorBoundary>
167
+ );
168
+ };
169
+ }
@@ -0,0 +1,78 @@
1
+ import { forwardRef } from "react";
2
+ import { TextInput, TextInputProps } from "react-native";
3
+ import {
4
+ Controller,
5
+ Control,
6
+ FieldValues,
7
+ Path,
8
+ RegisterOptions,
9
+ } from "react-hook-form";
10
+ import { Input } from "@/components/ui/Input";
11
+ import { Ionicons } from "@expo/vector-icons";
12
+
13
+ interface FormInputProps<T extends FieldValues>
14
+ extends Omit<TextInputProps, "value" | "onChangeText"> {
15
+ name: Path<T>;
16
+ control: Control<T>;
17
+ rules?: RegisterOptions<T, Path<T>>;
18
+ label?: string;
19
+ hint?: string;
20
+ leftIcon?: keyof typeof Ionicons.glyphMap;
21
+ rightIcon?: keyof typeof Ionicons.glyphMap;
22
+ onRightIconPress?: () => void;
23
+ containerClassName?: string;
24
+ inputClassName?: string;
25
+ }
26
+
27
+ /**
28
+ * Form-connected Input component using React Hook Form
29
+ * Automatically handles validation errors and form state
30
+ */
31
+ export const FormInput = forwardRef(function FormInputInner<
32
+ T extends FieldValues
33
+ >(
34
+ {
35
+ name,
36
+ control,
37
+ rules,
38
+ label,
39
+ hint,
40
+ leftIcon,
41
+ rightIcon,
42
+ onRightIconPress,
43
+ containerClassName,
44
+ inputClassName,
45
+ ...props
46
+ }: FormInputProps<T>,
47
+ ref: React.Ref<TextInput>
48
+ ) {
49
+ return (
50
+ <Controller
51
+ name={name}
52
+ control={control}
53
+ rules={rules}
54
+ render={({
55
+ field: { onChange, onBlur, value },
56
+ fieldState: { error },
57
+ }) => (
58
+ <Input
59
+ ref={ref}
60
+ label={label}
61
+ value={value}
62
+ onChangeText={onChange}
63
+ onBlur={onBlur}
64
+ error={error?.message}
65
+ hint={hint}
66
+ leftIcon={leftIcon}
67
+ rightIcon={rightIcon}
68
+ onRightIconPress={onRightIconPress}
69
+ containerClassName={containerClassName}
70
+ inputClassName={inputClassName}
71
+ {...props}
72
+ />
73
+ )}
74
+ />
75
+ );
76
+ }) as <T extends FieldValues>(
77
+ props: FormInputProps<T> & { ref?: React.Ref<TextInput> }
78
+ ) => JSX.Element;
@@ -0,0 +1 @@
1
+ export { FormInput } from "./FormInput";