@croacroa/react-native-template 2.1.0 → 3.2.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.
- package/.env.example +5 -0
- package/.eslintrc.js +8 -0
- package/.github/workflows/ci.yml +187 -187
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/.github/workflows/npm-publish.yml +57 -0
- package/CHANGELOG.md +195 -106
- package/CONTRIBUTING.md +377 -377
- package/LICENSE +21 -21
- package/README.md +446 -402
- package/__tests__/accessibility/components.test.tsx +285 -0
- package/__tests__/components/Button.test.tsx +2 -4
- package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
- package/__tests__/components/snapshots.test.tsx +131 -131
- package/__tests__/helpers/a11y.ts +54 -0
- package/__tests__/hooks/useAnalytics.test.ts +100 -0
- package/__tests__/hooks/useAnimations.test.ts +70 -0
- package/__tests__/hooks/useAuth.test.tsx +71 -28
- package/__tests__/hooks/useMedia.test.ts +318 -0
- package/__tests__/hooks/usePayments.test.tsx +307 -0
- package/__tests__/hooks/usePermission.test.ts +230 -0
- package/__tests__/hooks/useWebSocket.test.ts +329 -0
- package/__tests__/integration/auth-api.test.tsx +224 -227
- package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
- package/__tests__/services/api.test.ts +24 -6
- package/app/(auth)/home.tsx +11 -9
- package/app/(auth)/profile.tsx +8 -6
- package/app/(auth)/settings.tsx +11 -9
- package/app/(public)/forgot-password.tsx +25 -15
- package/app/(public)/login.tsx +48 -12
- package/app/(public)/onboarding.tsx +5 -5
- package/app/(public)/register.tsx +24 -15
- package/app/_layout.tsx +6 -3
- package/app.config.ts +27 -2
- package/assets/images/.gitkeep +7 -7
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/components/ErrorBoundary.tsx +73 -28
- package/components/auth/SocialLoginButtons.tsx +168 -0
- package/components/forms/FormInput.tsx +5 -3
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/AnalyticsProvider.tsx +67 -0
- package/components/providers/SuspenseBoundary.tsx +359 -357
- package/components/providers/index.ts +24 -21
- package/components/ui/AnimatedButton.tsx +1 -9
- package/components/ui/AnimatedList.tsx +98 -0
- package/components/ui/AnimatedScreen.tsx +89 -0
- package/components/ui/Avatar.tsx +319 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Button.tsx +11 -3
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/FeatureGate.tsx +57 -0
- package/components/ui/ForceUpdateScreen.tsx +108 -0
- package/components/ui/ImagePickerButton.tsx +180 -0
- package/components/ui/Input.stories.tsx +2 -10
- package/components/ui/Input.tsx +2 -10
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Paywall.tsx +253 -0
- package/components/ui/PermissionGate.tsx +155 -0
- package/components/ui/PurchaseButton.tsx +84 -0
- package/components/ui/Select.tsx +240 -240
- package/components/ui/Skeleton.tsx +3 -1
- package/components/ui/Toast.tsx +427 -418
- package/components/ui/UploadProgress.tsx +189 -0
- package/components/ui/VirtualizedList.tsx +288 -285
- package/components/ui/index.ts +28 -30
- package/constants/config.ts +135 -97
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/docs/guides/analytics-posthog.md +121 -0
- package/docs/guides/auth-supabase.md +162 -0
- package/docs/guides/feature-flags-launchdarkly.md +150 -0
- package/docs/guides/payments-revenuecat.md +169 -0
- package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
- package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
- package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
- package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
- package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
- package/eas.json +2 -1
- package/hooks/index.ts +70 -40
- package/hooks/useAnimatedEntry.ts +204 -0
- package/hooks/useApi.ts +5 -4
- package/hooks/useAuth.tsx +7 -3
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useChannel.ts +111 -0
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useExperiment.ts +36 -0
- package/hooks/useFeatureFlag.ts +59 -0
- package/hooks/useForceUpdate.ts +91 -0
- package/hooks/useImagePicker.ts +281 -375
- package/hooks/useInAppReview.ts +64 -0
- package/hooks/useMFA.ts +509 -499
- package/hooks/useParallax.ts +142 -0
- package/hooks/usePerformance.ts +434 -434
- package/hooks/usePermission.ts +190 -0
- package/hooks/usePresence.ts +129 -0
- package/hooks/useProducts.ts +36 -0
- package/hooks/usePurchase.ts +103 -0
- package/hooks/useRateLimit.ts +70 -0
- package/hooks/useSubscription.ts +49 -0
- package/hooks/useTrackEvent.ts +52 -0
- package/hooks/useTrackScreen.ts +40 -0
- package/hooks/useUpdates.ts +358 -358
- package/hooks/useUpload.ts +165 -0
- package/hooks/useWebSocket.ts +111 -0
- package/i18n/index.ts +197 -194
- package/i18n/locales/ar.json +170 -101
- package/i18n/locales/de.json +170 -101
- package/i18n/locales/en.json +170 -101
- package/i18n/locales/es.json +170 -101
- package/i18n/locales/fr.json +170 -101
- package/jest.config.js +1 -1
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -92
- package/maestro/flows/mfa-setup.yaml +86 -86
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -101
- package/maestro/flows/offline-sync.yaml +128 -128
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +188 -176
- package/scripts/generate-placeholders.js +38 -0
- package/services/analytics/adapters/console.ts +50 -0
- package/services/analytics/analytics-adapter.ts +94 -0
- package/services/analytics/types.ts +73 -0
- package/services/analytics.ts +428 -428
- package/services/api.ts +419 -340
- package/services/auth/social/apple.ts +110 -0
- package/services/auth/social/google.ts +159 -0
- package/services/auth/social/social-auth.ts +100 -0
- package/services/auth/social/types.ts +80 -0
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +652 -626
- package/services/feature-flags/adapters/mock.ts +108 -0
- package/services/feature-flags/feature-flag-adapter.ts +174 -0
- package/services/feature-flags/types.ts +79 -0
- package/services/force-update.ts +140 -0
- package/services/index.ts +116 -54
- package/services/media/compression.ts +91 -0
- package/services/media/media-picker.ts +151 -0
- package/services/media/media-upload.ts +160 -0
- package/services/payments/adapters/mock.ts +159 -0
- package/services/payments/payment-adapter.ts +118 -0
- package/services/payments/types.ts +131 -0
- package/services/permissions/permission-manager.ts +284 -0
- package/services/permissions/types.ts +104 -0
- package/services/realtime/types.ts +100 -0
- package/services/realtime/websocket-manager.ts +441 -0
- package/services/security.ts +289 -286
- package/services/sentry.ts +4 -4
- package/stores/appStore.ts +9 -0
- package/stores/notificationStore.ts +3 -1
- package/tailwind.config.js +47 -47
- package/tsconfig.json +37 -13
- package/types/user.ts +1 -1
- package/utils/accessibility.ts +446 -446
- package/utils/animations/presets.ts +182 -0
- package/utils/animations/transitions.ts +62 -0
- package/utils/index.ts +63 -52
- package/utils/toast.ts +9 -2
- package/utils/validation.ts +4 -1
- package/utils/withAccessibility.tsx +272 -272
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Image selection button with preview
|
|
3
|
+
* Shows a dashed placeholder when empty and the selected image with
|
|
4
|
+
* a clear button when an image is chosen.
|
|
5
|
+
* @module components/ui/ImagePickerButton
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { View, Text, Pressable, ActivityIndicator } from "react-native";
|
|
9
|
+
import { Image } from "expo-image";
|
|
10
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
11
|
+
import { useTheme } from "@/hooks/useTheme";
|
|
12
|
+
import {
|
|
13
|
+
useImagePicker,
|
|
14
|
+
type UseImagePickerOptions,
|
|
15
|
+
} from "@/hooks/useImagePicker";
|
|
16
|
+
import type { PickedMedia } from "@/services/media/media-picker";
|
|
17
|
+
import { cn } from "@/utils/cn";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for the ImagePickerButton component
|
|
21
|
+
*/
|
|
22
|
+
interface ImagePickerButtonProps {
|
|
23
|
+
/** Callback when an image is selected */
|
|
24
|
+
onImageSelected?: (media: PickedMedia) => void;
|
|
25
|
+
/** Size of the button in pixels (default: 100) */
|
|
26
|
+
size?: number;
|
|
27
|
+
/** Placeholder text (default: 'Add Photo') */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** Additional class name for the container */
|
|
30
|
+
className?: string;
|
|
31
|
+
/** Compression options passed to useImagePicker */
|
|
32
|
+
pickerOptions?: UseImagePickerOptions;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Image picker button with built-in preview.
|
|
37
|
+
* Displays a dashed border placeholder with a camera icon when no image is selected.
|
|
38
|
+
* Shows the selected image with an X button to clear when an image is chosen.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* function ProfileForm() {
|
|
43
|
+
* const [avatar, setAvatar] = useState<PickedMedia | null>(null);
|
|
44
|
+
*
|
|
45
|
+
* return (
|
|
46
|
+
* <ImagePickerButton
|
|
47
|
+
* size={120}
|
|
48
|
+
* placeholder="Profile Photo"
|
|
49
|
+
* onImageSelected={setAvatar}
|
|
50
|
+
* />
|
|
51
|
+
* );
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function ImagePickerButton({
|
|
56
|
+
onImageSelected,
|
|
57
|
+
size = 100,
|
|
58
|
+
placeholder = "Add Photo",
|
|
59
|
+
className,
|
|
60
|
+
pickerOptions,
|
|
61
|
+
}: ImagePickerButtonProps) {
|
|
62
|
+
const { isDark } = useTheme();
|
|
63
|
+
const { pickFromLibrary, pickFromCamera, selectedMedia, isLoading, clear } =
|
|
64
|
+
useImagePicker(pickerOptions);
|
|
65
|
+
|
|
66
|
+
const handlePress = async () => {
|
|
67
|
+
const result = await pickFromLibrary();
|
|
68
|
+
if (result && onImageSelected) {
|
|
69
|
+
onImageSelected(result);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleLongPress = async () => {
|
|
74
|
+
const result = await pickFromCamera();
|
|
75
|
+
if (result && onImageSelected) {
|
|
76
|
+
onImageSelected(result);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleClear = () => {
|
|
81
|
+
clear();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Loading state
|
|
85
|
+
if (isLoading) {
|
|
86
|
+
return (
|
|
87
|
+
<View
|
|
88
|
+
className={cn("items-center justify-center rounded-xl", className)}
|
|
89
|
+
style={{ width: size, height: size }}
|
|
90
|
+
>
|
|
91
|
+
<ActivityIndicator
|
|
92
|
+
size="small"
|
|
93
|
+
color={isDark ? "#94a3b8" : "#64748b"}
|
|
94
|
+
/>
|
|
95
|
+
</View>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Image selected state
|
|
100
|
+
if (selectedMedia) {
|
|
101
|
+
return (
|
|
102
|
+
<View
|
|
103
|
+
className={cn("relative", className)}
|
|
104
|
+
style={{ width: size, height: size }}
|
|
105
|
+
>
|
|
106
|
+
<View
|
|
107
|
+
className="overflow-hidden rounded-xl"
|
|
108
|
+
style={{ width: size, height: size }}
|
|
109
|
+
>
|
|
110
|
+
<Image
|
|
111
|
+
source={{ uri: selectedMedia.uri }}
|
|
112
|
+
style={{ width: size, height: size }}
|
|
113
|
+
contentFit="cover"
|
|
114
|
+
transition={200}
|
|
115
|
+
/>
|
|
116
|
+
</View>
|
|
117
|
+
|
|
118
|
+
{/* Clear button */}
|
|
119
|
+
<Pressable
|
|
120
|
+
onPress={handleClear}
|
|
121
|
+
className={cn(
|
|
122
|
+
"absolute -right-2 -top-2 z-10 items-center justify-center rounded-full",
|
|
123
|
+
isDark ? "bg-gray-700" : "bg-white"
|
|
124
|
+
)}
|
|
125
|
+
style={{
|
|
126
|
+
width: 24,
|
|
127
|
+
height: 24,
|
|
128
|
+
shadowColor: "#000",
|
|
129
|
+
shadowOffset: { width: 0, height: 1 },
|
|
130
|
+
shadowOpacity: 0.2,
|
|
131
|
+
shadowRadius: 2,
|
|
132
|
+
elevation: 3,
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<Ionicons
|
|
136
|
+
name="close"
|
|
137
|
+
size={16}
|
|
138
|
+
color={isDark ? "#f87171" : "#ef4444"}
|
|
139
|
+
/>
|
|
140
|
+
</Pressable>
|
|
141
|
+
</View>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Empty placeholder state
|
|
146
|
+
return (
|
|
147
|
+
<Pressable
|
|
148
|
+
onPress={handlePress}
|
|
149
|
+
onLongPress={handleLongPress}
|
|
150
|
+
className={cn(
|
|
151
|
+
"items-center justify-center rounded-xl",
|
|
152
|
+
isDark
|
|
153
|
+
? "border-gray-600 bg-gray-800/50"
|
|
154
|
+
: "border-gray-300 bg-gray-50",
|
|
155
|
+
className
|
|
156
|
+
)}
|
|
157
|
+
style={{
|
|
158
|
+
width: size,
|
|
159
|
+
height: size,
|
|
160
|
+
borderWidth: 2,
|
|
161
|
+
borderStyle: "dashed",
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<Ionicons
|
|
165
|
+
name="camera-outline"
|
|
166
|
+
size={size * 0.28}
|
|
167
|
+
color={isDark ? "#94a3b8" : "#9ca3af"}
|
|
168
|
+
/>
|
|
169
|
+
<Text
|
|
170
|
+
className={cn(
|
|
171
|
+
"mt-1 text-center text-xs",
|
|
172
|
+
isDark ? "text-gray-400" : "text-gray-500"
|
|
173
|
+
)}
|
|
174
|
+
numberOfLines={1}
|
|
175
|
+
>
|
|
176
|
+
{placeholder}
|
|
177
|
+
</Text>
|
|
178
|
+
</Pressable>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -91,16 +91,8 @@ export const AllStates: Story = {
|
|
|
91
91
|
placeholder="Enter text..."
|
|
92
92
|
hint="This is a helpful hint"
|
|
93
93
|
/>
|
|
94
|
-
<Input
|
|
95
|
-
|
|
96
|
-
placeholder="Enter password"
|
|
97
|
-
secureTextEntry
|
|
98
|
-
/>
|
|
99
|
-
<Input
|
|
100
|
-
label="With Icon"
|
|
101
|
-
placeholder="Search..."
|
|
102
|
-
leftIcon="search"
|
|
103
|
-
/>
|
|
94
|
+
<Input label="Password" placeholder="Enter password" secureTextEntry />
|
|
95
|
+
<Input label="With Icon" placeholder="Search..." leftIcon="search" />
|
|
104
96
|
</View>
|
|
105
97
|
),
|
|
106
98
|
};
|
package/components/ui/Input.tsx
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { forwardRef, useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
View,
|
|
4
|
-
Text,
|
|
5
|
-
TextInput,
|
|
6
|
-
TextInputProps,
|
|
7
|
-
Pressable,
|
|
8
|
-
} from "react-native";
|
|
2
|
+
import { View, Text, TextInput, TextInputProps, Pressable } from "react-native";
|
|
9
3
|
import { Ionicons } from "@expo/vector-icons";
|
|
10
4
|
import { cn } from "@/utils/cn";
|
|
11
5
|
import { useTheme } from "@/hooks/useTheme";
|
|
@@ -100,9 +94,7 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
100
94
|
) : null}
|
|
101
95
|
</View>
|
|
102
96
|
|
|
103
|
-
{error &&
|
|
104
|
-
<Text className="mt-1 text-sm text-red-500">{error}</Text>
|
|
105
|
-
)}
|
|
97
|
+
{error && <Text className="mt-1 text-sm text-red-500">{error}</Text>}
|
|
106
98
|
|
|
107
99
|
{hint && !error && (
|
|
108
100
|
<Text className="mt-1 text-sm text-muted-light dark:text-muted-dark">
|