@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
|
@@ -1,261 +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
|
-
}
|
|
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,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Feature-gated content component
|
|
3
|
+
* Renders children only when a feature flag is enabled,
|
|
4
|
+
* otherwise shows an optional fallback.
|
|
5
|
+
* @module components/ui/FeatureGate
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ReactNode } from "react";
|
|
9
|
+
|
|
10
|
+
import { useFeatureFlag } from "@/hooks/useFeatureFlag";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Props
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
interface FeatureGateProps {
|
|
17
|
+
/** The feature flag key to evaluate */
|
|
18
|
+
flag: string;
|
|
19
|
+
/** Content to render when the flag is enabled */
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
/** Optional content to render when the flag is disabled */
|
|
22
|
+
fallback?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Component
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Declarative component that shows or hides content based on a feature flag.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* // Basic usage
|
|
35
|
+
* <FeatureGate flag="new_dashboard">
|
|
36
|
+
* <NewDashboard />
|
|
37
|
+
* </FeatureGate>
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* // With fallback
|
|
43
|
+
* <FeatureGate flag="redesigned_profile" fallback={<OldProfile />}>
|
|
44
|
+
* <NewProfile />
|
|
45
|
+
* </FeatureGate>
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function FeatureGate({
|
|
49
|
+
flag,
|
|
50
|
+
children,
|
|
51
|
+
fallback = null,
|
|
52
|
+
}: FeatureGateProps) {
|
|
53
|
+
const { isEnabled, isLoading } = useFeatureFlag(flag);
|
|
54
|
+
|
|
55
|
+
if (isLoading) return null;
|
|
56
|
+
return isEnabled ? <>{children}</> : <>{fallback}</>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Full-screen non-dismissible force update screen
|
|
3
|
+
* Displayed when the running app version is below the server's minimum.
|
|
4
|
+
* @module components/ui/ForceUpdateScreen
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { View, Text, Pressable } from "react-native";
|
|
8
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
9
|
+
import * as Linking from "expo-linking";
|
|
10
|
+
import { useTranslation } from "react-i18next";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
interface ForceUpdateScreenProps {
|
|
17
|
+
/** Store URL to open when the user taps "Update Now" */
|
|
18
|
+
storeUrl: string;
|
|
19
|
+
/** The version currently running */
|
|
20
|
+
currentVersion: string;
|
|
21
|
+
/** The minimum version required by the server */
|
|
22
|
+
minimumVersion: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Component
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Full-screen, non-dismissible view that blocks the app until the user
|
|
31
|
+
* updates to the minimum required version.
|
|
32
|
+
*
|
|
33
|
+
* Renders as a top-level screen (not a Modal) so the user cannot navigate
|
|
34
|
+
* around it. The "Update Now" button opens the appropriate store URL.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* if (isUpdateRequired) {
|
|
39
|
+
* return (
|
|
40
|
+
* <ForceUpdateScreen
|
|
41
|
+
* storeUrl={storeUrl}
|
|
42
|
+
* currentVersion={currentVersion}
|
|
43
|
+
* minimumVersion={minimumVersion}
|
|
44
|
+
* />
|
|
45
|
+
* );
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function ForceUpdateScreen({
|
|
50
|
+
storeUrl,
|
|
51
|
+
currentVersion,
|
|
52
|
+
minimumVersion,
|
|
53
|
+
}: ForceUpdateScreenProps) {
|
|
54
|
+
const { t } = useTranslation();
|
|
55
|
+
|
|
56
|
+
const handleUpdate = () => {
|
|
57
|
+
Linking.openURL(storeUrl);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View className="flex-1 items-center justify-center bg-white px-8 dark:bg-gray-900">
|
|
62
|
+
{/* Icon */}
|
|
63
|
+
<View className="mb-8 h-24 w-24 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900">
|
|
64
|
+
<Ionicons name="cloud-download-outline" size={48} color="#3b82f6" />
|
|
65
|
+
</View>
|
|
66
|
+
|
|
67
|
+
{/* Title */}
|
|
68
|
+
<Text className="mb-4 text-center text-2xl font-bold text-gray-900 dark:text-white">
|
|
69
|
+
{t("forceUpdate.title")}
|
|
70
|
+
</Text>
|
|
71
|
+
|
|
72
|
+
{/* Message */}
|
|
73
|
+
<Text className="mb-8 text-center text-base leading-6 text-gray-600 dark:text-gray-400">
|
|
74
|
+
{t("forceUpdate.message")}
|
|
75
|
+
</Text>
|
|
76
|
+
|
|
77
|
+
{/* Version info */}
|
|
78
|
+
<View className="mb-8 w-full rounded-xl bg-gray-100 p-4 dark:bg-gray-800">
|
|
79
|
+
<View className="mb-2 flex-row items-center justify-between">
|
|
80
|
+
<Text className="text-sm text-gray-500 dark:text-gray-400">
|
|
81
|
+
{t("forceUpdate.currentVersion")}
|
|
82
|
+
</Text>
|
|
83
|
+
<Text className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
84
|
+
{currentVersion}
|
|
85
|
+
</Text>
|
|
86
|
+
</View>
|
|
87
|
+
<View className="flex-row items-center justify-between">
|
|
88
|
+
<Text className="text-sm text-gray-500 dark:text-gray-400">
|
|
89
|
+
{t("forceUpdate.minimumVersion")}
|
|
90
|
+
</Text>
|
|
91
|
+
<Text className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
92
|
+
{minimumVersion}
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
</View>
|
|
96
|
+
|
|
97
|
+
{/* Update button */}
|
|
98
|
+
<Pressable
|
|
99
|
+
onPress={handleUpdate}
|
|
100
|
+
className="w-full items-center rounded-xl bg-primary-600 px-6 py-4 active:bg-primary-700"
|
|
101
|
+
>
|
|
102
|
+
<Text className="text-lg font-semibold text-white">
|
|
103
|
+
{t("forceUpdate.button")}
|
|
104
|
+
</Text>
|
|
105
|
+
</Pressable>
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
}
|