@croacroa/react-native-template 2.0.1 → 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.
Files changed (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -0
  10. package/README.md +446 -399
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -0
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -23
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -27
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +64 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -0
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -175
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -0,0 +1,204 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { ViewStyle } from "react-native";
3
+ import {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withTiming,
7
+ withDelay,
8
+ interpolate,
9
+ } from "react-native-reanimated";
10
+ import type { WithTimingConfig, SharedValue } from "react-native-reanimated";
11
+
12
+ import {
13
+ TIMING,
14
+ ENTRY_CONFIGS,
15
+ staggerDelay,
16
+ } from "@/utils/animations/presets";
17
+ import type { EntryAnimation } from "@/utils/animations/presets";
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ export interface UseAnimatedEntryOptions {
24
+ /**
25
+ * Which entry animation to use
26
+ * @default 'fadeIn'
27
+ */
28
+ animation?: EntryAnimation;
29
+
30
+ /**
31
+ * Delay before the animation starts (ms)
32
+ * @default 0
33
+ */
34
+ delay?: number;
35
+
36
+ /**
37
+ * Timing configuration for the animation
38
+ * @default TIMING.normal
39
+ */
40
+ timing?: WithTimingConfig;
41
+
42
+ /**
43
+ * Whether the animation plays automatically on mount
44
+ * @default true
45
+ */
46
+ autoPlay?: boolean;
47
+ }
48
+
49
+ export interface UseAnimatedEntryReturn {
50
+ /** Animated style to spread onto an Animated.View */
51
+ animatedStyle: ViewStyle;
52
+
53
+ /** Manually trigger the entry animation */
54
+ play: () => void;
55
+
56
+ /** Reset animation back to start (progress = 0) */
57
+ reset: () => void;
58
+
59
+ /** Shared value tracking animation progress (0 = start, 1 = done) */
60
+ progress: SharedValue<number>;
61
+ }
62
+
63
+ // ============================================================================
64
+ // Hook Implementation
65
+ // ============================================================================
66
+
67
+ /**
68
+ * Hook for declarative entry animations.
69
+ *
70
+ * Returns an animated style that interpolates opacity, translateX, translateY,
71
+ * and scale from the chosen preset's initial values to their identity values
72
+ * as `progress` goes from 0 to 1.
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * function MyComponent() {
77
+ * const { animatedStyle } = useAnimatedEntry({
78
+ * animation: 'slideUp',
79
+ * delay: 200,
80
+ * });
81
+ *
82
+ * return (
83
+ * <Animated.View style={animatedStyle}>
84
+ * <Text>Hello</Text>
85
+ * </Animated.View>
86
+ * );
87
+ * }
88
+ * ```
89
+ */
90
+ export function useAnimatedEntry(
91
+ options: UseAnimatedEntryOptions = {}
92
+ ): UseAnimatedEntryReturn {
93
+ const {
94
+ animation = "fadeIn",
95
+ delay = 0,
96
+ timing = TIMING.normal,
97
+ autoPlay = true,
98
+ } = options;
99
+
100
+ const progress = useSharedValue(0);
101
+ const config = ENTRY_CONFIGS[animation];
102
+
103
+ // Store timing config in a ref to avoid infinite re-renders when
104
+ // callers pass an inline object literal for `timing`.
105
+ const timingRef = useRef(timing);
106
+ timingRef.current = timing;
107
+
108
+ const play = useCallback(() => {
109
+ progress.value = 0;
110
+ if (delay > 0) {
111
+ progress.value = withDelay(delay, withTiming(1, timingRef.current));
112
+ } else {
113
+ progress.value = withTiming(1, timingRef.current);
114
+ }
115
+ }, [delay, progress]);
116
+
117
+ const reset = useCallback(() => {
118
+ progress.value = 0;
119
+ }, [progress]);
120
+
121
+ // Auto-play on mount
122
+ useEffect(() => {
123
+ if (autoPlay) {
124
+ play();
125
+ }
126
+ }, [autoPlay, play]);
127
+
128
+ const animatedStyle = useAnimatedStyle((): ViewStyle => {
129
+ return {
130
+ opacity: interpolate(progress.value, [0, 1], [config.opacity, 1]),
131
+ transform: [
132
+ {
133
+ translateX: interpolate(
134
+ progress.value,
135
+ [0, 1],
136
+ [config.translateX, 0]
137
+ ),
138
+ },
139
+ {
140
+ translateY: interpolate(
141
+ progress.value,
142
+ [0, 1],
143
+ [config.translateY, 0]
144
+ ),
145
+ },
146
+ {
147
+ scale: interpolate(progress.value, [0, 1], [config.scale, 1]),
148
+ },
149
+ ],
150
+ };
151
+ });
152
+
153
+ return { animatedStyle, play, reset, progress };
154
+ }
155
+
156
+ // ============================================================================
157
+ // Staggered Entry
158
+ // ============================================================================
159
+
160
+ export interface UseStaggeredEntryOptions extends Omit<
161
+ UseAnimatedEntryOptions,
162
+ "delay"
163
+ > {
164
+ /**
165
+ * Base delay between each staggered item (ms)
166
+ * @default 50
167
+ */
168
+ staggerDelay?: number;
169
+ }
170
+
171
+ /**
172
+ * Convenience wrapper around `useAnimatedEntry` that adds an index-based
173
+ * stagger delay. Use this for list items that should animate in sequentially.
174
+ *
175
+ * @param index - The item's position in the list (0-based)
176
+ * @param options - Same as useAnimatedEntry options, plus `staggerDelay`
177
+ *
178
+ * @example
179
+ * ```tsx
180
+ * function ListItem({ index }: { index: number }) {
181
+ * const { animatedStyle } = useStaggeredEntry(index, {
182
+ * animation: 'slideUp',
183
+ * staggerDelay: 80,
184
+ * });
185
+ *
186
+ * return (
187
+ * <Animated.View style={animatedStyle}>
188
+ * <Text>Item {index}</Text>
189
+ * </Animated.View>
190
+ * );
191
+ * }
192
+ * ```
193
+ */
194
+ export function useStaggeredEntry(
195
+ index: number,
196
+ options: UseStaggeredEntryOptions = {}
197
+ ): UseAnimatedEntryReturn {
198
+ const { staggerDelay: baseDelay = 50, ...rest } = options;
199
+
200
+ return useAnimatedEntry({
201
+ ...rest,
202
+ delay: staggerDelay(index, baseDelay),
203
+ });
204
+ }
package/hooks/useApi.ts CHANGED
@@ -8,8 +8,10 @@ import {
8
8
  useQuery,
9
9
  useMutation,
10
10
  useQueryClient,
11
+ useSuspenseQuery,
11
12
  UseQueryOptions,
12
13
  UseMutationOptions,
14
+ UseSuspenseQueryOptions,
13
15
  } from "@tanstack/react-query";
14
16
  import { api } from "@/services/api";
15
17
  import { toast, handleApiError } from "@/utils/toast";
@@ -36,14 +38,15 @@ export const queryKeys = {
36
38
  },
37
39
  posts: {
38
40
  all: ["posts"] as const,
39
- list: (filters: Record<string, unknown>) => ["posts", "list", filters] as const,
41
+ list: (filters: Record<string, unknown>) =>
42
+ ["posts", "list", filters] as const,
40
43
  detail: (id: string) => ["posts", id] as const,
41
44
  },
42
45
  // Add more query keys as needed
43
46
  } as const;
44
47
 
45
48
  // Generic types for API responses
46
- interface PaginatedResponse<T> {
49
+ interface _PaginatedResponse<T> {
47
50
  data: T[];
48
51
  page: number;
49
52
  pageSize: number;
@@ -120,6 +123,63 @@ export function useUser(
120
123
  });
121
124
  }
122
125
 
126
+ // ===========================================
127
+ // Suspense-Ready Hooks (React 19 compatible)
128
+ // ===========================================
129
+
130
+ /**
131
+ * Suspense-ready version of useCurrentUser.
132
+ * Use inside a Suspense boundary - throws promise while loading.
133
+ *
134
+ * @example
135
+ * ```tsx
136
+ * function Profile() {
137
+ * const { data: user } = useSuspenseCurrentUser();
138
+ * // No loading check needed - Suspense handles it
139
+ * return <Text>Hello, {user.name}</Text>;
140
+ * }
141
+ *
142
+ * // Wrap with Suspense
143
+ * <Suspense fallback={<Skeleton />}>
144
+ * <Profile />
145
+ * </Suspense>
146
+ * ```
147
+ */
148
+ export function useSuspenseCurrentUser(
149
+ options?: Omit<UseSuspenseQueryOptions<User, Error>, "queryKey" | "queryFn">
150
+ ) {
151
+ return useSuspenseQuery({
152
+ queryKey: queryKeys.users.me(),
153
+ queryFn: () => api.get<User>("/users/me"),
154
+ staleTime: 1000 * 60 * 5,
155
+ ...options,
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Suspense-ready version of useUser.
161
+ * Use inside a Suspense boundary - throws promise while loading.
162
+ *
163
+ * @param userId - The unique identifier of the user to fetch
164
+ * @example
165
+ * ```tsx
166
+ * function UserCard({ userId }: { userId: string }) {
167
+ * const { data: user } = useSuspenseUser(userId);
168
+ * return <Avatar name={user.name} />;
169
+ * }
170
+ * ```
171
+ */
172
+ export function useSuspenseUser(
173
+ userId: string,
174
+ options?: Omit<UseSuspenseQueryOptions<User, Error>, "queryKey" | "queryFn">
175
+ ) {
176
+ return useSuspenseQuery({
177
+ queryKey: queryKeys.users.detail(userId),
178
+ queryFn: () => api.get<User>(`/users/${userId}`),
179
+ ...options,
180
+ });
181
+ }
182
+
123
183
  /**
124
184
  * Update the current user's profile.
125
185
  * Automatically updates the cache and shows a success/error toast.
@@ -163,7 +223,7 @@ export function useUpdateUser() {
163
223
  // Generic CRUD Hooks Factory
164
224
  // ===========================================
165
225
 
166
- interface CrudHooksConfig<T, CreateDTO, UpdateDTO> {
226
+ interface CrudHooksConfig<_T, _CreateDTO, _UpdateDTO> {
167
227
  baseKey: readonly string[];
168
228
  endpoint: string;
169
229
  entityName: string;
@@ -205,7 +265,7 @@ interface CrudHooksConfig<T, CreateDTO, UpdateDTO> {
205
265
  export function createCrudHooks<
206
266
  T extends { id: string },
207
267
  CreateDTO = Omit<T, "id">,
208
- UpdateDTO = Partial<T>
268
+ UpdateDTO = Partial<T>,
209
269
  >(config: CrudHooksConfig<T, CreateDTO, UpdateDTO>) {
210
270
  const { baseKey, endpoint, entityName } = config;
211
271
 
package/hooks/useAuth.tsx CHANGED
@@ -128,7 +128,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
128
128
  // Check if tokens are expired
129
129
  if (parsedTokens.expiresAt < Date.now()) {
130
130
  // Try to refresh
131
- const refreshed = await tryRefreshWithToken(parsedTokens.refreshToken);
131
+ const refreshed = await tryRefreshWithToken(
132
+ parsedTokens.refreshToken
133
+ );
132
134
  if (!refreshed) {
133
135
  await clearAuth();
134
136
  return;
@@ -146,7 +148,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
146
148
  }
147
149
  };
148
150
 
149
- const tryRefreshWithToken = async (refreshToken: string): Promise<boolean> => {
151
+ const tryRefreshWithToken = async (
152
+ _refreshToken: string
153
+ ): Promise<boolean> => {
150
154
  try {
151
155
  // TODO: Replace with your actual API call
152
156
  // const response = await api.post('/auth/refresh', { refreshToken });
@@ -190,7 +194,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
190
194
  setUser(null);
191
195
  };
192
196
 
193
- const signIn = useCallback(async (email: string, password: string) => {
197
+ const signIn = useCallback(async (email: string, _password: string) => {
194
198
  try {
195
199
  // TODO: Replace with your actual API call
196
200
  // const response = await api.post('/auth/login', { email, password });