@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,261 @@
1
+ import { TouchableOpacity, View, Text } from "react-native";
2
+ import Animated, {
3
+ useAnimatedStyle,
4
+ withSpring,
5
+ withTiming,
6
+ interpolateColor,
7
+ } from "react-native-reanimated";
8
+ import { Ionicons } from "@expo/vector-icons";
9
+ import { useTheme } from "@/hooks/useTheme";
10
+ import { cn } from "@/utils/cn";
11
+
12
+ interface CheckboxProps {
13
+ /**
14
+ * Whether the checkbox is checked
15
+ */
16
+ checked: boolean;
17
+
18
+ /**
19
+ * Callback when the checkbox is toggled
20
+ */
21
+ onChange: (checked: boolean) => void;
22
+
23
+ /**
24
+ * Label text
25
+ */
26
+ label?: string;
27
+
28
+ /**
29
+ * Description text (below label)
30
+ */
31
+ description?: string;
32
+
33
+ /**
34
+ * Whether the checkbox is disabled
35
+ */
36
+ disabled?: boolean;
37
+
38
+ /**
39
+ * Size variant
40
+ */
41
+ size?: "sm" | "md" | "lg";
42
+
43
+ /**
44
+ * Additional class name
45
+ */
46
+ className?: string;
47
+
48
+ /**
49
+ * Error message
50
+ */
51
+ error?: string;
52
+ }
53
+
54
+ const AnimatedView = Animated.createAnimatedComponent(View);
55
+
56
+ const sizes = {
57
+ sm: { box: 18, icon: 12, label: "text-sm" },
58
+ md: { box: 22, icon: 16, label: "text-base" },
59
+ lg: { box: 26, icon: 20, label: "text-lg" },
60
+ };
61
+
62
+ export function Checkbox({
63
+ checked,
64
+ onChange,
65
+ label,
66
+ description,
67
+ disabled = false,
68
+ size = "md",
69
+ className,
70
+ error,
71
+ }: CheckboxProps) {
72
+ const { isDark } = useTheme();
73
+ const sizeConfig = sizes[size];
74
+
75
+ const animatedBoxStyle = useAnimatedStyle(() => {
76
+ const backgroundColor = interpolateColor(
77
+ checked ? 1 : 0,
78
+ [0, 1],
79
+ ["transparent", "#10b981"]
80
+ );
81
+
82
+ const borderColor = interpolateColor(
83
+ checked ? 1 : 0,
84
+ [0, 1],
85
+ [error ? "#ef4444" : isDark ? "#475569" : "#cbd5e1", "#10b981"]
86
+ );
87
+
88
+ return {
89
+ backgroundColor: withTiming(backgroundColor, { duration: 150 }),
90
+ borderColor: withTiming(borderColor, { duration: 150 }),
91
+ transform: [
92
+ {
93
+ scale: withSpring(checked ? 1 : 0.95, {
94
+ damping: 15,
95
+ stiffness: 400,
96
+ }),
97
+ },
98
+ ],
99
+ };
100
+ }, [checked, isDark, error]);
101
+
102
+ const animatedCheckStyle = useAnimatedStyle(() => {
103
+ return {
104
+ opacity: withTiming(checked ? 1 : 0, { duration: 150 }),
105
+ transform: [
106
+ {
107
+ scale: withSpring(checked ? 1 : 0.5, {
108
+ damping: 15,
109
+ stiffness: 400,
110
+ }),
111
+ },
112
+ ],
113
+ };
114
+ }, [checked]);
115
+
116
+ return (
117
+ <TouchableOpacity
118
+ onPress={() => !disabled && onChange(!checked)}
119
+ disabled={disabled}
120
+ activeOpacity={0.7}
121
+ className={cn(
122
+ "flex-row items-start",
123
+ disabled && "opacity-50",
124
+ className
125
+ )}
126
+ >
127
+ <AnimatedView
128
+ style={[
129
+ animatedBoxStyle,
130
+ {
131
+ width: sizeConfig.box,
132
+ height: sizeConfig.box,
133
+ borderWidth: 2,
134
+ borderRadius: 6,
135
+ justifyContent: "center",
136
+ alignItems: "center",
137
+ marginTop: 2,
138
+ },
139
+ ]}
140
+ >
141
+ <Animated.View style={animatedCheckStyle}>
142
+ <Ionicons name="checkmark" size={sizeConfig.icon} color="white" />
143
+ </Animated.View>
144
+ </AnimatedView>
145
+
146
+ {(label || description) && (
147
+ <View className="flex-1 ml-3">
148
+ {label && (
149
+ <Text
150
+ className={cn(
151
+ sizeConfig.label,
152
+ isDark ? "text-text-dark" : "text-text-light",
153
+ disabled && "opacity-70"
154
+ )}
155
+ >
156
+ {label}
157
+ </Text>
158
+ )}
159
+ {description && (
160
+ <Text
161
+ className={cn(
162
+ "text-sm mt-0.5",
163
+ isDark ? "text-muted-dark" : "text-muted-light"
164
+ )}
165
+ >
166
+ {description}
167
+ </Text>
168
+ )}
169
+ {error && <Text className="text-red-500 text-sm mt-1">{error}</Text>}
170
+ </View>
171
+ )}
172
+ </TouchableOpacity>
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Checkbox Group component for multiple checkboxes
178
+ */
179
+ interface CheckboxGroupProps<T extends string> {
180
+ /**
181
+ * Available options
182
+ */
183
+ options: {
184
+ value: T;
185
+ label: string;
186
+ description?: string;
187
+ disabled?: boolean;
188
+ }[];
189
+
190
+ /**
191
+ * Currently selected values
192
+ */
193
+ value: T[];
194
+
195
+ /**
196
+ * Callback when selection changes
197
+ */
198
+ onChange: (value: T[]) => void;
199
+
200
+ /**
201
+ * Group label
202
+ */
203
+ label?: string;
204
+
205
+ /**
206
+ * Size variant
207
+ */
208
+ size?: "sm" | "md" | "lg";
209
+
210
+ /**
211
+ * Additional class name
212
+ */
213
+ className?: string;
214
+ }
215
+
216
+ export function CheckboxGroup<T extends string>({
217
+ options,
218
+ value,
219
+ onChange,
220
+ label,
221
+ size = "md",
222
+ className,
223
+ }: CheckboxGroupProps<T>) {
224
+ const { isDark } = useTheme();
225
+
226
+ const handleToggle = (optionValue: T) => {
227
+ if (value.includes(optionValue)) {
228
+ onChange(value.filter((v) => v !== optionValue));
229
+ } else {
230
+ onChange([...value, optionValue]);
231
+ }
232
+ };
233
+
234
+ return (
235
+ <View className={className}>
236
+ {label && (
237
+ <Text
238
+ className={cn(
239
+ "text-sm font-medium mb-3",
240
+ isDark ? "text-text-dark" : "text-text-light"
241
+ )}
242
+ >
243
+ {label}
244
+ </Text>
245
+ )}
246
+ <View className="gap-3">
247
+ {options.map((option) => (
248
+ <Checkbox
249
+ key={option.value}
250
+ checked={value.includes(option.value)}
251
+ onChange={() => handleToggle(option.value)}
252
+ label={option.label}
253
+ description={option.description}
254
+ disabled={option.disabled}
255
+ size={size}
256
+ />
257
+ ))}
258
+ </View>
259
+ </View>
260
+ );
261
+ }
@@ -0,0 +1,106 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { View } from "react-native";
3
+ import { Input } from "./Input";
4
+
5
+ const meta: Meta<typeof Input> = {
6
+ title: "UI/Input",
7
+ component: Input,
8
+ argTypes: {
9
+ label: {
10
+ control: "text",
11
+ },
12
+ placeholder: {
13
+ control: "text",
14
+ },
15
+ error: {
16
+ control: "text",
17
+ },
18
+ hint: {
19
+ control: "text",
20
+ },
21
+ secureTextEntry: {
22
+ control: "boolean",
23
+ },
24
+ },
25
+ args: {
26
+ label: "Label",
27
+ placeholder: "Enter text...",
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof Input>;
33
+
34
+ export const Default: Story = {
35
+ args: {
36
+ label: "Email",
37
+ placeholder: "Enter your email",
38
+ },
39
+ };
40
+
41
+ export const WithIcon: Story = {
42
+ args: {
43
+ label: "Search",
44
+ placeholder: "Search...",
45
+ leftIcon: "search",
46
+ },
47
+ };
48
+
49
+ export const Password: Story = {
50
+ args: {
51
+ label: "Password",
52
+ placeholder: "Enter password",
53
+ secureTextEntry: true,
54
+ },
55
+ };
56
+
57
+ export const WithError: Story = {
58
+ args: {
59
+ label: "Email",
60
+ placeholder: "Enter your email",
61
+ value: "invalid-email",
62
+ error: "Please enter a valid email address",
63
+ },
64
+ };
65
+
66
+ export const WithHint: Story = {
67
+ args: {
68
+ label: "Username",
69
+ placeholder: "Choose a username",
70
+ hint: "Username must be 3-20 characters",
71
+ },
72
+ };
73
+
74
+ export const AllStates: Story = {
75
+ render: () => (
76
+ <View style={{ gap: 16 }}>
77
+ <Input label="Default" placeholder="Enter text..." />
78
+ <Input
79
+ label="With Value"
80
+ placeholder="Enter text..."
81
+ value="Hello World"
82
+ />
83
+ <Input
84
+ label="With Error"
85
+ placeholder="Enter text..."
86
+ value="Invalid"
87
+ error="This field has an error"
88
+ />
89
+ <Input
90
+ label="With Hint"
91
+ placeholder="Enter text..."
92
+ hint="This is a helpful hint"
93
+ />
94
+ <Input
95
+ label="Password"
96
+ placeholder="Enter password"
97
+ secureTextEntry
98
+ />
99
+ <Input
100
+ label="With Icon"
101
+ placeholder="Search..."
102
+ leftIcon="search"
103
+ />
104
+ </View>
105
+ ),
106
+ };
@@ -0,0 +1,117 @@
1
+ import { forwardRef, useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ TextInput,
6
+ TextInputProps,
7
+ Pressable,
8
+ } from "react-native";
9
+ import { Ionicons } from "@expo/vector-icons";
10
+ import { cn } from "@/utils/cn";
11
+ import { useTheme } from "@/hooks/useTheme";
12
+
13
+ interface InputProps extends TextInputProps {
14
+ label?: string;
15
+ error?: string;
16
+ hint?: string;
17
+ leftIcon?: keyof typeof Ionicons.glyphMap;
18
+ rightIcon?: keyof typeof Ionicons.glyphMap;
19
+ onRightIconPress?: () => void;
20
+ containerClassName?: string;
21
+ inputClassName?: string;
22
+ }
23
+
24
+ export const Input = forwardRef<TextInput, InputProps>(
25
+ (
26
+ {
27
+ label,
28
+ error,
29
+ hint,
30
+ leftIcon,
31
+ rightIcon,
32
+ onRightIconPress,
33
+ containerClassName,
34
+ inputClassName,
35
+ secureTextEntry,
36
+ ...props
37
+ },
38
+ ref
39
+ ) => {
40
+ const { isDark } = useTheme();
41
+ const [isPasswordVisible, setIsPasswordVisible] = useState(false);
42
+ const isPassword = secureTextEntry !== undefined;
43
+
44
+ const togglePasswordVisibility = () => {
45
+ setIsPasswordVisible(!isPasswordVisible);
46
+ };
47
+
48
+ return (
49
+ <View className={cn("w-full", containerClassName)}>
50
+ {label && (
51
+ <Text className="mb-2 text-sm font-medium text-text-light dark:text-text-dark">
52
+ {label}
53
+ </Text>
54
+ )}
55
+
56
+ <View
57
+ className={cn(
58
+ "flex-row items-center rounded-xl border-2 bg-surface-light px-4 dark:bg-surface-dark",
59
+ error
60
+ ? "border-red-500"
61
+ : "border-gray-200 focus-within:border-primary-500 dark:border-gray-700"
62
+ )}
63
+ >
64
+ {leftIcon && (
65
+ <Ionicons
66
+ name={leftIcon}
67
+ size={20}
68
+ color={isDark ? "#94a3b8" : "#64748b"}
69
+ style={{ marginRight: 8 }}
70
+ />
71
+ )}
72
+
73
+ <TextInput
74
+ ref={ref}
75
+ className={cn(
76
+ "flex-1 py-3 text-base text-text-light dark:text-text-dark",
77
+ inputClassName
78
+ )}
79
+ placeholderTextColor={isDark ? "#64748b" : "#94a3b8"}
80
+ secureTextEntry={isPassword && !isPasswordVisible}
81
+ {...props}
82
+ />
83
+
84
+ {isPassword ? (
85
+ <Pressable onPress={togglePasswordVisibility}>
86
+ <Ionicons
87
+ name={isPasswordVisible ? "eye-off-outline" : "eye-outline"}
88
+ size={20}
89
+ color={isDark ? "#94a3b8" : "#64748b"}
90
+ />
91
+ </Pressable>
92
+ ) : rightIcon ? (
93
+ <Pressable onPress={onRightIconPress} disabled={!onRightIconPress}>
94
+ <Ionicons
95
+ name={rightIcon}
96
+ size={20}
97
+ color={isDark ? "#94a3b8" : "#64748b"}
98
+ />
99
+ </Pressable>
100
+ ) : null}
101
+ </View>
102
+
103
+ {error && (
104
+ <Text className="mt-1 text-sm text-red-500">{error}</Text>
105
+ )}
106
+
107
+ {hint && !error && (
108
+ <Text className="mt-1 text-sm text-muted-light dark:text-muted-dark">
109
+ {hint}
110
+ </Text>
111
+ )}
112
+ </View>
113
+ );
114
+ }
115
+ );
116
+
117
+ Input.displayName = "Input";
@@ -0,0 +1,98 @@
1
+ import {
2
+ Modal as RNModal,
3
+ View,
4
+ Text,
5
+ Pressable,
6
+ ModalProps as RNModalProps,
7
+ KeyboardAvoidingView,
8
+ Platform,
9
+ } from "react-native";
10
+ import { Ionicons } from "@expo/vector-icons";
11
+ import { cn } from "@/utils/cn";
12
+ import { useTheme } from "@/hooks/useTheme";
13
+
14
+ interface ModalProps extends Omit<RNModalProps, "children"> {
15
+ title?: string;
16
+ onClose: () => void;
17
+ showCloseButton?: boolean;
18
+ size?: "sm" | "md" | "lg" | "full";
19
+ className?: string;
20
+ children: React.ReactNode;
21
+ }
22
+
23
+ const sizeStyles = {
24
+ sm: "max-w-sm",
25
+ md: "max-w-md",
26
+ lg: "max-w-lg",
27
+ full: "w-full h-full",
28
+ };
29
+
30
+ export function Modal({
31
+ visible,
32
+ title,
33
+ onClose,
34
+ showCloseButton = true,
35
+ size = "md",
36
+ className,
37
+ children,
38
+ ...props
39
+ }: ModalProps) {
40
+ const { isDark } = useTheme();
41
+
42
+ return (
43
+ <RNModal
44
+ visible={visible}
45
+ transparent
46
+ animationType="fade"
47
+ onRequestClose={onClose}
48
+ {...props}
49
+ >
50
+ <KeyboardAvoidingView
51
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
52
+ className="flex-1"
53
+ >
54
+ <Pressable
55
+ onPress={onClose}
56
+ className="flex-1 items-center justify-center bg-black/50 px-4"
57
+ >
58
+ <Pressable
59
+ onPress={(e) => e.stopPropagation()}
60
+ className={cn(
61
+ "w-full rounded-2xl bg-background-light p-6 dark:bg-background-dark",
62
+ size !== "full" && sizeStyles[size],
63
+ className
64
+ )}
65
+ >
66
+ {/* Header */}
67
+ {(title || showCloseButton) && (
68
+ <View className="mb-4 flex-row items-center justify-between">
69
+ {title ? (
70
+ <Text className="text-xl font-semibold text-text-light dark:text-text-dark">
71
+ {title}
72
+ </Text>
73
+ ) : (
74
+ <View />
75
+ )}
76
+ {showCloseButton && (
77
+ <Pressable
78
+ onPress={onClose}
79
+ className="h-8 w-8 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800"
80
+ >
81
+ <Ionicons
82
+ name="close"
83
+ size={20}
84
+ color={isDark ? "#f8fafc" : "#0f172a"}
85
+ />
86
+ </Pressable>
87
+ )}
88
+ </View>
89
+ )}
90
+
91
+ {/* Content */}
92
+ {children}
93
+ </Pressable>
94
+ </Pressable>
95
+ </KeyboardAvoidingView>
96
+ </RNModal>
97
+ );
98
+ }